From 2d08164f7a003bab695156d551bcbb847cbfd700 Mon Sep 17 00:00:00 2001 From: Oleksandr Tarasiuk Date: Fri, 9 Oct 2026 11:06:13 +0300 Subject: [PATCH] feat: add missing properties and JSX attributes code fixes --- tsc/internal/checker/exports.go | 20 + tsc/internal/fourslash/fourslash.go | 4 +- .../codeFixAddMissingAttributes10_test.go | 9 +- .../codeFixAddMissingAttributes11_test.go | 147 +++ .../codeFixAddMissingAttributes12_test.go | 255 +++++ .../codeFixAddMissingAttributes1_test.go | 43 + .../codeFixAddMissingAttributes2_test.go | 35 + .../codeFixAddMissingAttributes3_test.go | 37 + .../codeFixAddMissingAttributes4_test.go | 38 + .../codeFixAddMissingAttributes5_test.go | 9 +- .../codeFixAddMissingAttributes6_test.go | 9 +- .../codeFixAddMissingAttributes7_test.go | 35 + .../codeFixAddMissingAttributes8_test.go | 34 + .../codeFixAddMissingAttributes9_test.go | 34 + .../codeFixAddMissingAttributes_all_test.go | 60 ++ .../codeFixAddMissingProperties10_test.go | 991 ++++++++++++++++++ .../codeFixAddMissingProperties11_test.go | 360 +++++++ .../codeFixAddMissingProperties1_test.go | 29 + .../codeFixAddMissingProperties2_test.go | 110 ++ .../codeFixAddMissingProperties3_test.go | 143 +++ .../codeFixAddMissingProperties4_test.go | 75 ++ .../codeFixAddMissingProperties5_test.go | 312 ++++++ .../codeFixAddMissingProperties6_test.go | 151 +++ .../codeFixAddMissingProperties7_test.go | 51 + .../codeFixAddMissingProperties8_test.go | 42 + .../codeFixAddMissingProperties9_test.go | 51 + tsc/internal/ls/autoimport/fix.go | 63 +- tsc/internal/ls/autoimport/import_adder.go | 50 +- tsc/internal/ls/change/tracker.go | 53 +- tsc/internal/ls/change/trackerimpl.go | 16 +- tsc/internal/ls/codeactions.go | 147 ++- .../ls/codeactions_addmissingattributes.go | 159 +++ .../ls/codeactions_addmissingproperties.go | 235 +++++ ..._fixclassincorrectlyimplementsinterface.go | 65 +- .../codeactions_fixmissingtypeannotation.go | 42 +- tsc/internal/ls/codeactions_importfixes.go | 35 +- .../ls/codeactions_missingmemberfixer.go | 221 +++- tsc/internal/ls/completions.go | 18 +- .../testutil/contentmappertest/editing.go | 5 +- 39 files changed, 3974 insertions(+), 219 deletions(-) create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes11_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes12_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes1_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes2_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes3_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes4_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes7_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes8_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes9_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingAttributes_all_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties10_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties11_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties1_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties2_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties3_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties4_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties5_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties6_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties7_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties8_test.go create mode 100644 tsc/internal/fourslash/tests/codeFixAddMissingProperties9_test.go create mode 100644 tsc/internal/ls/codeactions_addmissingattributes.go create mode 100644 tsc/internal/ls/codeactions_addmissingproperties.go diff --git a/tsc/internal/checker/exports.go b/tsc/internal/checker/exports.go index f77488e783142..da326ffcc487f 100644 --- a/tsc/internal/checker/exports.go +++ b/tsc/internal/checker/exports.go @@ -152,6 +152,18 @@ func (c *Checker) GetPropertiesOfType(t *Type) []*ast.Symbol { return c.getPropertiesOfType(t) } +func (c *Checker) GetJsxAttributesType(node *ast.Node) *Type { + return c.checkExpressionCached(node) +} + +func (c *Checker) GetUnmatchedProperties(source *Type, target *Type, requireOptionalProperties bool, matchDiscriminantProperties bool) []*ast.Symbol { + return c.getUnmatchedProperties(source, target, requireOptionalProperties, matchDiscriminantProperties) +} + +func (c *Checker) IsDeeplyNestedType(t *Type, stack []*Type, maxDepth int) bool { + return c.isDeeplyNestedType(t, stack, maxDepth) +} + func (c *Checker) GetPropertyOfType(t *Type, name string) *ast.Symbol { return c.getPropertyOfType(t, name) } @@ -172,6 +184,10 @@ func (c *Checker) IsPropertyAccessible(node *ast.Node, isSuper bool, isWrite boo return c.isPropertyAccessible(node, isSuper, isWrite, containingType, property) } +func (c *Checker) IsConstructorAccessible(node *ast.Node, signatures []*Signature) bool { + return c.getConstructorAccessibilityError(node, signatures, ast.ModifierFlagsNonPublicAccessibilityModifier) == nil +} + func (c *Checker) GetTypeOfPropertyOfContextualType(t *Type, name string) *Type { return c.getTypeOfPropertyOfContextualType(t, name) } @@ -188,6 +204,10 @@ func (c *Checker) GetSignaturesOfType(t *Type, kind SignatureKind) []*Signature return c.getSignaturesOfType(t, kind) } +func (c *Checker) GetMinArgumentCount(signature *Signature) int { + return c.getMinArgumentCount(signature) +} + func (c *Checker) GetDeclaredTypeOfSymbol(symbol *ast.Symbol) *Type { return c.getDeclaredTypeOfSymbol(symbol) } diff --git a/tsc/internal/fourslash/fourslash.go b/tsc/internal/fourslash/fourslash.go index 7c8fbe3191648..951c6b257b73d 100644 --- a/tsc/internal/fourslash/fourslash.go +++ b/tsc/internal/fourslash/fourslash.go @@ -1687,6 +1687,7 @@ type VerifyCodeFixOptions struct { // VerifyCodeFixAllOptions are the options for VerifyCodeFixAll. type VerifyCodeFixAllOptions struct { FixID string + Description string NewFileContent string } @@ -1931,7 +1932,8 @@ func (f *FourslashTest) VerifyCodeFixAll(t *testing.T, options VerifyCodeFixAllO // We look for actions that are NOT single-diagnostic fixes (i.e., have no Diagnostics attached). var fixAllCandidates []*lsproto.CodeAction for _, action := range actions { - if action.Diagnostics == nil || len(*action.Diagnostics) == 0 { + if (action.Diagnostics == nil || len(*action.Diagnostics) == 0) && + (options.Description == "" || action.Title == options.Description) { fixAllCandidates = append(fixAllCandidates, action) } } diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes10_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes10_test.go index a6932a4fd605d..3114624246256 100644 --- a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes10_test.go +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes10_test.go @@ -3,7 +3,9 @@ package fourslash_test import ( "testing" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/testutil" ) @@ -20,8 +22,9 @@ const A = (props: { [K in D]: K }) =>
; const Bar = () => - [||]` - f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) defer done() - f.VerifyCodeFixNotAvailable(t, "fixMissingAttributes") + f.VerifyNumberOfErrorsInCurrentFile(t, 1) + f.VerifyCodeFixNotAvailable(t, diagnostics.Add_missing_attributes.Localize(locale.Default)) } diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes11_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes11_test.go new file mode 100644 index 0000000000000..339e26c6429c9 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes11_test.go @@ -0,0 +1,147 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes11(t *testing.T) { + t.Parallel() + for i, test := range []struct { + content string + result string + fileResult string + filename string + }{ + { + content: `// @jsx: preserve +// @Filename: a.tsx +interface T { a: number; b?: string } +const A = (a: T) => null; +const a = ;`, + result: ` a={0}`, + }, + { + content: `// @jsx: preserve +// @Filename: /a.ts +export enum E { a, b } +export class C {} +// @Filename: /b.tsx +import { E as B, C as D } from "./a"; +const A = (a: { a: B; b: D }) => null; +const a = ;`, + result: ` a={B.a} b={new D}`, + filename: "/b.tsx", + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +declare namespace JSX { + interface Element {} + interface ElementChildrenAttribute { children: {} } +} +const A = (a: { children: string; b: number }) => null; +const a = text;`, + result: ` b={0}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +declare namespace JSX { + interface Element {} + interface ElementChildrenAttribute { c: {} } +} +const A = (a: { c: string; b: number }) => null; +const a = {"text"};`, + result: ` b={0}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +declare namespace JSX { + interface Element {} + interface ElementChildrenAttribute { children: {} } +} +const A = (a: { children: string; b: number }) => null; +const a = {};`, + result: ` children={""} b={0}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: number; b: string }) => null; +declare const b: { a?: number }; +const a = ;`, + result: ` a={0} b={""}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: number; b: string }) => null; +declare const b: { a: number } | {}; +const a = ;`, + result: ` a={0} b={""}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: number; b: string }) => null; +declare const b: { a?: number }; +const a = ;`, + result: ` b={""}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: number }) => null; +declare const b: { a?: number }; +const a = ;`, + result: ` a={0}`, + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: number; b: string }) => null; +declare const b: { a?: number }; +declare const c: { a: number }; +const a = ;`, + result: ` b={""}`, + }, + { + content: `// @jsx: preserve +// @Filename: /a.ts +export enum E { a, b } +export class C {} +// @Filename: /b.tsx +import type { C as D, E as B } from "./a"; +const A = (a: { a: B; b: D }) => null; +const a = ;`, + fileResult: `import { C as D, E as B } from "./a"; +const A = (a: { a: B; b: D }) => null; +const a = ;`, + filename: "/b.tsx", + }, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + if test.filename != "" { + f.GoToFile(t, test.filename) + } + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewFileContent: test.fileResult, + NewRangeContent: test.result, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes12_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes12_test.go new file mode 100644 index 0000000000000..eb38c6003826f --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes12_test.go @@ -0,0 +1,255 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes12(t *testing.T) { + t.Parallel() + const content = `// @jsx: preserve +// @Filename: a.tsx +interface T { a: number } +const A = (a: T) => null; +const a: T = {}; +const b: T = {}; +const c = ; +const d = ;` + for i, test := range []struct { + fixID string + description string + result string + errors int + }{ + { + fixID: "fixMissingProperties", + description: diagnostics.Add_all_missing_properties.Localize(locale.Default), + result: `interface T { a: number } +const A = (a: T) => null; +const a: T = { + a: 0 +}; +const b: T = { + a: 0 +}; +const c = ; +const d = ;`, + errors: 2, + }, + { + fixID: "fixMissingAttributes", + description: diagnostics.Add_all_missing_attributes.Localize(locale.Default), + result: `interface T { a: number } +const A = (a: T) => null; +const a: T = {}; +const b: T = {}; +const c = ; +const d = ;`, + errors: 2, + }, + { + result: `interface T { a: number } +const A = (a: T) => null; +const a: T = { + a: 0 +}; +const b: T = { + a: 0 +}; +const c = ; +const d = ;`, + }, + } { + t.Run(strconv.Itoa(i+1), 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.VerifyNumberOfErrorsInCurrentFile(t, 4) + if test.fixID == "" { + f.VerifySourceFixAll(t, test.result) + } else { + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: test.fixID, + Description: test.description, + NewFileContent: test.result, + }) + } + f.VerifyNumberOfErrorsInCurrentFile(t, test.errors) + }) + } + for i, test := range []struct { + content string + result string + filename string + }{ + { + content: `// @jsx: preserve +// @Filename: /a.ts +export class C {} +export enum E { a, b } +// @Filename: /b.tsx +import type { C, E } from "./a"; +const A = (a: { a: E }) => null; +const a: { a: C } = {}; +const b = ;`, + result: `import { C, E } from "./a"; +const A = (a: { a: E }) => null; +const a: { a: C } = { + a: new C +}; +const b = ;`, + filename: "/b.tsx", + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: { a: number }, b: number }) => null; +const a = ({}))()} />;`, + result: `const A = (a: { a: { a: number }, b: number }) => null; +const a = ({ + a: 0 +}))()} b={0} />;`, + filename: "a.tsx", + }, + { + content: `// @jsx: preserve +// @verbatimModuleSyntax: true +// @Filename: /a.ts +export class C {} +export enum E { a, b } +export interface T { a: number } +// @Filename: /b.tsx +import type { C, E, T } from "./a"; +const A = (a: { a: E }) => null; +const a: { a: C } = {}; +const b = ; +const c = E.b; +const d: T = { a: 0 };`, + result: `import { C, E, type T } from "./a"; +const A = (a: { a: E }) => null; +const a: { a: C } = { + a: new C +}; +const b = ; +const c = E.b; +const d: T = { a: 0 };`, + filename: "/b.tsx", + }, + { + content: `// @jsx: preserve +// @lib: es2015 +// @Filename: /a.ts +export const a = Symbol(); +export class C {} +export enum E { a, b } +export type T = { [a]: C }; +export type U = { a: E }; +// @Filename: /b.tsx +import type { T, U } from "./a"; +const A = (a: U) => null; +const b: T = {}; +const c = ;`, + result: `import { a, C, E, type T, type U } from "./a"; +const A = (a: U) => null; +const b: T = { + [a]: new C +}; +const c = ;`, + filename: "/b.tsx", + }, + { + content: `// @jsx: preserve +// @Filename: a.tsx +const A = (a: { a: number, b: number }) => null; +const a = ({}))()} />;`, + result: `const A = (a: { a: number, b: number }) => null; +const a = ({ + a: 0 +}))()} />;`, + }, + { + content: `// @jsx: preserve +// @strictPropertyInitialization: false +// @Filename: /a.ts +export enum E { a, b } +export interface T { a: E } +// @Filename: /b.tsx +import type { T } from "./a"; +class C implements T {} +const A = (a: T) => null; +const a = ;`, + result: `import { E, type T } from "./a"; +class C implements T { + a: E; +} +const A = (a: T) => null; +const a = ;`, + filename: "/b.tsx", + }, + { + content: `// @jsx: preserve +// @declaration: true +// @isolatedDeclarations: true +// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.tsx +import type { E } from "./a"; +const A = (a: { a: E }) => null; +export function a() { return Math.abs(0); } +const b: { a: E } = {}; +const c = ;`, + result: `import { E } from "./a"; +const A = (a: { a: E }) => null; +export function a(): number { return Math.abs(0); } +const b: { a: E } = { + a: E.a +}; +const c = ;`, + filename: "/b.tsx", + }, + { + content: `// @jsx: preserve +// @declaration: true +// @isolatedDeclarations: true +// @verbatimModuleSyntax: true +// @Filename: /a.ts +export class C {} +export interface T { a: C } +export interface U { a: number } +// @Filename: /b.tsx +import type { T } from "./a"; +declare function a(): import("./a").U; +const A = (a: T) => null; +export function b() { return a(); } +const c: T = {}; +const d = ;`, + result: `import { C, type T, type U } from "./a"; +declare function a(): import("./a").U; +const A = (a: T) => null; +export function b(): U { return a(); } +const c: T = { + a: new C +}; +const d = ;`, + filename: "/b.tsx", + }, + } { + t.Run(strconv.Itoa(i+4), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + if test.filename != "" { + f.GoToFile(t, test.filename) + } + f.VerifySourceFixAll(t, test.result) + f.VerifyNoErrors(t) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes1_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes1_test.go new file mode 100644 index 0000000000000..a455d7d7038f2 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes1_test.go @@ -0,0 +1,43 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +const addMissingAttributesJsxDeclarations = ` +declare namespace JSX { + interface Element {} + interface IntrinsicElements { + div: {}; + } +}` + +func TestCodeFixAddMissingAttributes1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +interface P { + a: number; + b: string; +} + +const A = ({ a, b }: P) => +
{a}{b}
; + +const B = () => +
` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` a={0} b={""}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes2_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes2_test.go new file mode 100644 index 0000000000000..59bd0064d7b68 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes2_test.go @@ -0,0 +1,35 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes2(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +interface P { + a: number; + b: string; +} + +const A = ({ a, b }: P) => +
{a}{b}
; + +const B = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` b={""}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes3_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes3_test.go new file mode 100644 index 0000000000000..675082a5850a4 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes3_test.go @@ -0,0 +1,37 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes3(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +interface P { + a: number; + b: string; + c: number[]; + d: any; +} + +const A = ({ a, b, c, d }: P) => +
{a}{b}{c}{d}
; + +const B = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` c={[]} d={undefined}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes4_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes4_test.go new file mode 100644 index 0000000000000..f993f9800ea3c --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes4_test.go @@ -0,0 +1,38 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes4(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +interface P { + a: number; + b: string; + c: number[]; + d: any; +} + +const A = ({ a, b, c, d }: P) => +
{a}{b}{c}{d}
; + +const b = { a: 1, c: [] }; +const B = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` b={""} d={undefined}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes5_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes5_test.go index 8060af4c7a9b0..845247ca693fd 100644 --- a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes5_test.go +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes5_test.go @@ -3,7 +3,9 @@ package fourslash_test import ( "testing" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/testutil" ) @@ -23,8 +25,9 @@ const A = ({ a, b, c, d }: P) =>
{a}{b}{c}{d}
; const Bar = () => - [||]` - f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) defer done() - f.VerifyCodeFixNotAvailable(t, "fixMissingAttributes") + f.VerifyNoErrors(t) + f.VerifyCodeFixNotAvailable(t, diagnostics.Add_missing_attributes.Localize(locale.Default)) } diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes6_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes6_test.go index 3d389911e22e1..6a34cf775f382 100644 --- a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes6_test.go +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes6_test.go @@ -3,7 +3,9 @@ package fourslash_test import ( "testing" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/testutil" ) @@ -24,8 +26,9 @@ const A = ({ a, b, c, d }: P) => const props = { a: 1, b: "", c: [], d: undefined }; const Bar = () => - [||]` - f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) defer done() - f.VerifyCodeFixNotAvailable(t, "fixMissingAttributes") + f.VerifyNoErrors(t) + f.VerifyCodeFixNotAvailable(t, diagnostics.Add_missing_attributes.Localize(locale.Default)) } diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes7_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes7_test.go new file mode 100644 index 0000000000000..94e7196efcf38 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes7_test.go @@ -0,0 +1,35 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes7(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +interface P { + a: number; + b?: string; +} + +const A = ({ a, b }: P) => +
{a}{b}
; + +const B = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` a={0}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes8_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes8_test.go new file mode 100644 index 0000000000000..b4abf133dd9cc --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes8_test.go @@ -0,0 +1,34 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes8(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +type A = 'a' | 'b'; +type B = 'd' | 'c'; +type C = ` + "`" + `${A}${B}` + "`" + `; + +const A = (b: { [K in C]: K }) => +
; + +const B = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` ac={"ac"} ad={"ad"} bc={"bc"} bd={"bd"}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes9_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes9_test.go new file mode 100644 index 0000000000000..bb7f50043bfdf --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes9_test.go @@ -0,0 +1,34 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes9(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +type A = 'a-' | 'b-'; +type B = 'd' | 'c'; +type C = ` + "`" + `${A}${B}` + "`" + `; + +const A = (b: { [K in C]: K }) => +
; + +const B = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_attributes.Localize(locale.Default), + NewRangeContent: ` a-c={"a-c"} a-d={"a-d"} b-c={"b-c"} b-d={"b-d"}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingAttributes_all_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes_all_test.go new file mode 100644 index 0000000000000..1226504133710 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingAttributes_all_test.go @@ -0,0 +1,60 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingAttributes_all(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @jsx: preserve +// @Filename: a.tsx +interface P { + a: number; + b: string; + c: number[]; + d: any; +} +const A = ({ a, b, c, d }: P) => +
{a}{b}{c}{d}
; +const b = { a: 1, b: "" }; + +const C1 = () => + +const C2 = () => + +const C3 = () => + +const C4 = () => + ` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content+addMissingAttributesJsxDeclarations) + defer done() + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "fixMissingAttributes", + Description: diagnostics.Add_all_missing_attributes.Localize(locale.Default), + NewFileContent: `interface P { + a: number; + b: string; + c: number[]; + d: any; +} +const A = ({ a, b, c, d }: P) => +
{a}{b}{c}{d}
; +const b = { a: 1, b: "" }; + +const C1 = () => + +const C2 = () => + +const C3 = () => + +const C4 = () => + ` + addMissingAttributesJsxDeclarations, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties10_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties10_test.go new file mode 100644 index 0000000000000..8f480ad057e90 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties10_test.go @@ -0,0 +1,991 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties10(t *testing.T) { + t.Parallel() + for i, test := range []struct { + content string + result string + filename string + fullFile bool + preferences *lsutil.UserPreferences + }{ + { + content: `// @strict: false +interface Foo { + a: number; + b: string; + c: 1; + d: "d"; + e: "e1" | "e2"; + f(x: number, y: number): void; + g: (x: number, y: number) => void; + h: number[]; + i: bigint; + j: undefined | "special-string"; + k: ` + "`" + `--${string}` + "`" + `; +} +const foo: Foo = [|{}|];`, + result: `{ + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + h: [], + i: 0n, + j: "special-string", + k: "" +}`, + }, + { + content: `interface Foo { + a: number; + b: string; + c: any; +} +class C { + public c: Foo = [|{}|]; +}`, + result: `{ + a: 0, + b: "", + c: undefined + }`, + }, + { + content: `interface Foo { + a: number; + b: string; +} +function fn(foo: Foo = [|{}|]) { +}`, + result: `{ + a: 0, + b: "" +}`, + }, + { + content: `interface Foo { + a: number; + b: string; +} +const foo: Foo = [|{ a: 10 }|];`, + result: `{ + a: 10, + b: "" +}`, + }, + { + content: `type T = { + a: null; +} + +const foo: T = [|{}|];`, + result: `{ + a: null +}`, + }, + { + content: `// @strict: false +interface I { + x: number; + y: number; +} +class C { + public p: number; + m(x: number, y: I) {} +} +const foo: C = [|{}|];`, + result: `{ + p: 0, + m: function(x: number, y: I): void { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `enum E1 { + A, B +} +enum E2 { + A +} +enum E3 { +} +interface I { + x: E1; + y: E2; + z: E3; +} +const foo: I = [|{}|];`, + result: `{ + x: E1.A, + y: E2.A, + z: 0 +}`, + }, + { + content: `class A { + constructor() {} +} + +abstract class B {} + +class C { + constructor(a: string, b: number, c: A) {} +} + +interface I { + a: A; + b: B; + c: C; +} +const foo: I = [|{}|];`, + result: `{ + a: new A, + b: undefined, + c: undefined +}`, + }, + { + content: `interface I { + a: { + x: number; + y: { z: string; }; + } +} +const foo: I = [|{}|];`, + result: `{ + a: { + x: 0, + y: { + z: "" + } + } +}`, + }, + { + content: `type T = { x: number; }; +interface I { + a: T +} +const foo: I = [|{}|];`, + result: `{ + a: { + x: 0 + } +}`, + }, + { + content: `interface Foo { + a: ` + "`" + `--${string}` + "`" + `; + b: string; + c: "a" | "b" +} +const foo: Foo = [|{}|];`, + result: `{ + a: '', + b: '', + c: 'a' +}`, + preferences: &lsutil.UserPreferences{QuotePreference: "single"}, + }, + { + content: `interface Foo { + a: number; + b: number; +} +function f(foo: Foo) {} +f([|{}|]);`, + result: `{ + a: 0, + b: 0 +}`, + }, + { + content: `interface Foo { + a: number; + b: number; + c: () => void; +} +function f(foo: Foo) {} +f([|{ a: 10 }|]);`, + result: `{ + a: 10, + b: 0, + c: function(): void { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `interface Foo { + a: number; + b: number; +} +function f(a: number, b: number, c: Foo) {} +f(1, 2, [|{}|]);`, + result: `{ + a: 0, + b: 0 +}`, + }, + { + content: `interface Foo { + 1: number; + 2: number; +} +const foo: Foo = [|{}|]`, + result: `{ + 1: 0, + 2: 0 +}`, + }, + { + content: `type A = 'a' | 'b' | 'c' | 'd' | 'e'; +type B = 1 | 2 | 3; +type C = '@' | '!'; +type D = ` + "`" + `${A}${Uppercase}${B}${C}` + "`" + `; + +const names: { [K in D]: K } = [|{}|];`, + result: `{ + "aA1!": "aA1!", + "aA1@": "aA1@", + "aA2!": "aA2!", + "aA2@": "aA2@", + "aA3!": "aA3!", + "aA3@": "aA3@", + "aB1!": "aB1!", + "aB1@": "aB1@", + "aB2!": "aB2!", + "aB2@": "aB2@", + "aB3!": "aB3!", + "aB3@": "aB3@", + "aC1!": "aC1!", + "aC1@": "aC1@", + "aC2!": "aC2!", + "aC2@": "aC2@", + "aC3!": "aC3!", + "aC3@": "aC3@", + "aD1!": "aD1!", + "aD1@": "aD1@", + "aD2!": "aD2!", + "aD2@": "aD2@", + "aD3!": "aD3!", + "aD3@": "aD3@", + "aE1!": "aE1!", + "aE1@": "aE1@", + "aE2!": "aE2!", + "aE2@": "aE2@", + "aE3!": "aE3!", + "aE3@": "aE3@", + "bA1!": "bA1!", + "bA1@": "bA1@", + "bA2!": "bA2!", + "bA2@": "bA2@", + "bA3!": "bA3!", + "bA3@": "bA3@", + "bB1!": "bB1!", + "bB1@": "bB1@", + "bB2!": "bB2!", + "bB2@": "bB2@", + "bB3!": "bB3!", + "bB3@": "bB3@", + "bC1!": "bC1!", + "bC1@": "bC1@", + "bC2!": "bC2!", + "bC2@": "bC2@", + "bC3!": "bC3!", + "bC3@": "bC3@", + "bD1!": "bD1!", + "bD1@": "bD1@", + "bD2!": "bD2!", + "bD2@": "bD2@", + "bD3!": "bD3!", + "bD3@": "bD3@", + "bE1!": "bE1!", + "bE1@": "bE1@", + "bE2!": "bE2!", + "bE2@": "bE2@", + "bE3!": "bE3!", + "bE3@": "bE3@", + "cA1!": "cA1!", + "cA1@": "cA1@", + "cA2!": "cA2!", + "cA2@": "cA2@", + "cA3!": "cA3!", + "cA3@": "cA3@", + "cB1!": "cB1!", + "cB1@": "cB1@", + "cB2!": "cB2!", + "cB2@": "cB2@", + "cB3!": "cB3!", + "cB3@": "cB3@", + "cC1!": "cC1!", + "cC1@": "cC1@", + "cC2!": "cC2!", + "cC2@": "cC2@", + "cC3!": "cC3!", + "cC3@": "cC3@", + "cD1!": "cD1!", + "cD1@": "cD1@", + "cD2!": "cD2!", + "cD2@": "cD2@", + "cD3!": "cD3!", + "cD3@": "cD3@", + "cE1!": "cE1!", + "cE1@": "cE1@", + "cE2!": "cE2!", + "cE2@": "cE2@", + "cE3!": "cE3!", + "cE3@": "cE3@", + "dA1!": "dA1!", + "dA1@": "dA1@", + "dA2!": "dA2!", + "dA2@": "dA2@", + "dA3!": "dA3!", + "dA3@": "dA3@", + "dB1!": "dB1!", + "dB1@": "dB1@", + "dB2!": "dB2!", + "dB2@": "dB2@", + "dB3!": "dB3!", + "dB3@": "dB3@", + "dC1!": "dC1!", + "dC1@": "dC1@", + "dC2!": "dC2!", + "dC2@": "dC2@", + "dC3!": "dC3!", + "dC3@": "dC3@", + "dD1!": "dD1!", + "dD1@": "dD1@", + "dD2!": "dD2!", + "dD2@": "dD2@", + "dD3!": "dD3!", + "dD3@": "dD3@", + "dE1!": "dE1!", + "dE1@": "dE1@", + "dE2!": "dE2!", + "dE2@": "dE2@", + "dE3!": "dE3!", + "dE3@": "dE3@", + "eA1!": "eA1!", + "eA1@": "eA1@", + "eA2!": "eA2!", + "eA2@": "eA2@", + "eA3!": "eA3!", + "eA3@": "eA3@", + "eB1!": "eB1!", + "eB1@": "eB1@", + "eB2!": "eB2!", + "eB2@": "eB2@", + "eB3!": "eB3!", + "eB3@": "eB3@", + "eC1!": "eC1!", + "eC1@": "eC1@", + "eC2!": "eC2!", + "eC2@": "eC2@", + "eC3!": "eC3!", + "eC3@": "eC3@", + "eD1!": "eD1!", + "eD1@": "eD1@", + "eD2!": "eD2!", + "eD2@": "eD2@", + "eD3!": "eD3!", + "eD3@": "eD3@", + "eE1!": "eE1!", + "eE1@": "eE1@", + "eE2!": "eE2!", + "eE2@": "eE2@", + "eE3!": "eE3!", + "eE3@": "eE3@" +}`, + }, + { + content: `interface Foo { + foo(): T; +} +const x: Foo = [|{}|];`, + result: `{ + foo: function(): string { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `interface Bar { + a: number; +} + +interface Foo { + foo(a: T): U; +} +const x: Foo = [|{}|];`, + result: `{ + foo: function(a: string): Bar { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `export interface Foo { + z(...args: T extends unknown[] ? T : [T]); +} +export interface Bar { + a(foo: Foo<[number]>): void; + b(foo: Foo<[]>): void; + c(foo: Foo): void; +} +const bar: Bar = [|{}|];`, + result: `{ + a: function(foo: Foo<[number]>): void { + throw new Error("Function not implemented."); + }, + b: function(foo: Foo<[]>): void { + throw new Error("Function not implemented."); + }, + c: function(foo: Foo): void { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `type T = { foo: number }; +const foo: T[] = []; +foo.push([|{ }|])`, + result: `{ + foo: 0 +}`, + }, + { + content: `// @Filename: /a.ts +export type A = { x: string }; +// @Filename: /b.ts +import { A } from "./a"; +export type Foo = { x: string }; +export interface B { + b(a: A): Foo; +} +// @Filename: /c.ts +import { B } from "./b"; +const b: B = {};`, + result: `import { A } from "./a"; +import { B, Foo } from "./b"; +const b: B = { + b: function(a: A): Foo { + throw new Error("Function not implemented."); + } +};`, + filename: "/c.ts", + fullFile: true, + }, + { + content: `// @lib: es2020 +const x: Iterable = {}`, + result: `const x: Iterable = { + [Symbol.iterator]: function(): Iterator { + throw new Error("Function not implemented."); + } +}`, + fullFile: true, + }, + { + content: `enum E { + A +} +let obj: Record = {}`, + result: `enum E { + A +} +let obj: Record = { + [E.A]: undefined +}`, + fullFile: true, + }, + { + content: `interface A { + a: number; + b: string; +} +interface B { + c: boolean; +} +interface C { + a: A; + b: B; +} +function f(type: T, obj: C[T]): string { + return ""; +} +f("a", [|{}|]);`, + result: `{ + a: 0, + b: "" +}`, + }, + { + content: `// @allowJs: true +// @checkJs: true +// @Filename: /a.js +/** + * @type {{ f: (x: string) => number }} + */ +export const foo = [|{}|]`, + result: `{ + f: function(x) { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `// @allowJs: true +// @checkJs: true +// @Filename: /a.js +/** + * @type {{ f: (x?: string) => number }} + */ +export const foo = [|{}|]`, + result: `{ + f: function(x) { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `interface Foo { + a?: boolean; + b: boolean; +} +type A = { [K in keyof Foo]-?: { name: K, value: ` + "`" + `Foo ${Uppercase}` + "`" + ` }} +const a: A = [|{}|];`, + result: `{ + a: { + name: "a", + value: "Foo A" + }, + b: { + name: "b", + value: "Foo B" + } +}`, + }, + { + content: `interface Foo { + a: number; + b: string; +} + +interface Bar { + value: Foo; +} + +const bar: Bar = { + value: [|{ + a: 10 + }|] +}`, + result: `{ + a: 10, + b: "" + }`, + }, + { + content: `namespace Foo.Bar { + export enum E { + E1 = 0, + E2 = 1, + } + export interface Baz { + prop1: string; + prop2: number; + prop3: E.E1; + prop4: Foo.Bar.E.E1; + } +} +const foo: Foo.Bar.Baz = [|{}|]`, + result: `{ + prop1: "", + prop2: 0, + prop3: Foo.Bar.E.E1, + prop4: Foo.Bar.E.E1 +}`, + }, + { + content: `interface A { + a: number; + b: string; +} +function f(_obj: A[]): string { + return ""; +} +f([[|{}|]])`, + result: `{ + a: 0, + b: "" +}`, + }, + { + content: `interface A { + a: number; + b: string; +} +interface B { + c: A[]; +} +const b: B[] = [{c: [[|{}|]]}]`, + result: `{ + a: 0, + b: "" +}`, + }, + { + content: `// @Filename: E.ts +export enum E { + A, + B, +} +// @Filename: foo.ts +import { E } from "./E" +type T = { + e: E, +} +const t: T = [|{ }|]`, + result: `{ + e: E.A +}`, + filename: "foo.ts", + }, + { + content: `// @strict: false +interface Foo { + a: number; + b: string; + c: 1; + d: "d"; + e: "e1" | "e2"; + f(x: number, y: number): void; + g: (x: number, y: number) => void; + h: number[]; + i: bigint; + j: undefined | "special-string"; + k: ` + "`" + `--${string}` + "`" + `; +} +const b = [|{}|] satisfies Foo;`, + result: `{ + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + h: [], + i: 0n, + j: "special-string", + k: "" +}`, + }, + { + content: `interface Foo { + a: number; + b: string; + c: any; +} +class C { + public c = [|{}|] satisfies Foo; +}`, + result: `{ + a: 0, + b: "", + c: undefined + }`, + }, + { + content: `interface Foo { + a: number; + b: string; +} +const foo = [|{ a: 10 }|] satisfies Foo;`, + result: `{ + a: 10, + b: "" +}`, + }, + { + content: `type T = { + a: null; +} + +const foo = [|{}|] satisfies T;`, + result: `{ + a: null +}`, + }, + { + content: `// @strict: false +interface I { + x: number; + y: number; +} +class C { + public p: number; + m(x: number, y: I) {} +} +const foo = [|{}|] satisfies C;`, + result: `{ + p: 0, + m: function(x: number, y: I): void { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `enum E1 { + A, B +} +enum E2 { + A +} +enum E3 { +} +interface I { + x: E1; + y: E2; + z: E3; +} +const foo = [|{}|] satisfies I;`, + result: `{ + x: E1.A, + y: E2.A, + z: 0 +}`, + }, + { + content: `class A { + constructor() {} +} + +abstract class B {} + +class C { + constructor(a: string, b: number, c: A) {} +} + +interface I { + a: A; + b: B; + c: C; +} +const foo = [|{}|] satisfies I;`, + result: `{ + a: new A, + b: undefined, + c: undefined +}`, + }, + { + content: `interface I { + a: { + x: number; + y: { z: string; }; + } +} +const foo = [|{}|] satisfies I;`, + result: `{ + a: { + x: 0, + y: { + z: "" + } + } +}`, + }, + { + content: `interface Bar { + a: number; +} + +interface Foo { + foo(a: T): U; +} +const x = [|{}|] satisfies Foo;`, + result: `{ + foo: function(a: string): Bar { + throw new Error("Function not implemented."); + } +}`, + }, + { + content: `type A = { a: string }; +type B = { b: string }; + +const c = [|{ }|] satisfies A satisfies B;`, + result: `{ + a: "" +}`, + }, + { + content: `// @strict: false +interface Foo { + a: number; + b: string; + c: 1; + d: "d"; + e: "e1" | "e2"; + f(x: number, y: number): void; + g: (x: number, y: number) => void; + h: number[]; + i: bigint; + j: undefined | "special-string"; + k: ` + "`" + `--${string}` + "`" + `; +} +const f = (): Foo => { + return [|{ }|]; +};`, + result: `{ + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + h: [], + i: 0n, + j: "special-string", + k: "" +}`, + }, + { + content: `// @strict: false +// @lib: es2020 +// @target: es2020 +interface Foo { + a: number; + b: string; + c: 1; + d: "d"; + e: "e1" | "e2"; + f(x: number, y: number): void; + g: (x: number, y: number) => void; + h: number[]; + i: bigint; + j: undefined | "special-string"; + k: ` + "`" + `--${string}` + "`" + `; +} +const f = function* (): Generator { + yield [|{}|]; +};`, + result: `{ + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + h: [], + i: 0n, + j: "special-string", + k: "" +}`, + }, + { + content: `// @strict: true + type U = { u?: { v: string } }; + const u: U = { u: [|{}|] };`, + result: `{ + v: "" +}`, + }, + { + content: `// @strict: true + type T = { t: string }; + declare function f(arg?: T): void; + f([|{}|]);`, + result: `{ + t: "" +}`, + }, + { + content: `// @strict: true + interface A { + a: number; + b: string; + } + function f(_obj: (A | undefined)[]): string { + return ""; + } + f([[|{}|]]);`, + result: `{ + a: 0, + b: "" +}`, + }, + { + content: `// @strict: true + interface A { + a: number; + b: string; + } + interface B { + c: (A | undefined)[]; + } + const b: B[] = [{ c: [[|{}|]] }];`, + result: `{ + a: 0, + b: "" +}`, + }, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + if test.filename != "" { + f.GoToFile(t, test.filename) + } + options := fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + ApplyChanges: true, + UserPreferences: test.preferences, + } + if test.fullFile { + options.NewFileContent = test.result + } else { + options.NewRangeContent = test.result + } + f.VerifyCodeFix(t, options) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties11_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties11_test.go new file mode 100644 index 0000000000000..b3d4c36469a68 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties11_test.go @@ -0,0 +1,360 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" + "github.com/microsoft/TypeScript/tsc/internal/testutil/contentmappertest" +) + +func TestCodeFixAddMissingProperties11(t *testing.T) { + t.Parallel() + t.Run("1", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `interface Test { + foo: string; + bar(a: string): void; +} +function f (_spec: any) {} +function g (_spec: Test) {} +f(() => { + g({}); + g( + {}); + g( + {} + ); +});` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "fixMissingProperties", + Description: diagnostics.Add_all_missing_properties.Localize(locale.Default), + NewFileContent: `interface Test { + foo: string; + bar(a: string): void; +} +function f (_spec: any) {} +function g (_spec: Test) {} +f(() => { + g({ + foo: "", + bar: function(a: string): void { + throw new Error("Function not implemented."); + } + }); + g( + { + foo: "", + bar: function(a: string): void { + throw new Error("Function not implemented."); + } + }); + g( + { + foo: "", + bar: function(a: string): void { + throw new Error("Function not implemented."); + } + } + ); +});`, + }) + f.VerifyNoErrors(t) + }) + t.Run("2", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @lib: es2015 +// @Filename: /a.ts +export const a = Symbol(); +export enum E { a, b } +export class C {} +export type T = { [a]: E; b: C }; +// @Filename: /b.ts +import type { C as B, E, T } from "./a"; +const b: { b: B; c: E; d?: T } = {}; +const c: T = {};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/b.ts") + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "fixMissingProperties", + Description: diagnostics.Add_all_missing_properties.Localize(locale.Default), + NewFileContent: `import { a, C as B, E, type T } from "./a"; +const b: { b: B; c: E; d?: T } = { + b: new B, + c: E.a +}; +const c: T = { + [a]: E.a, + b: new B +};`, + }) + f.VerifyNoErrors(t) + }) + t.Run("3", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type T = { a: () => { b: number }; c: number }; +const a: T = { a: (): { b: number } => ({}) };` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "fixMissingProperties", + Description: diagnostics.Add_all_missing_properties.Localize(locale.Default), + NewFileContent: `type T = { a: () => { b: number }; c: number }; +const a: T = { a: (): { b: number } => ({ + b: 0 +}), c: 0 };`, + }) + f.VerifyNoErrors(t) + }) + t.Run("4", func(t *testing.T) { + t.Parallel() + // Canonical output is export {}; the supplemental script maps the whole input verbatim. + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := newContentMapperFourslash(t, `// @Filename: /a.ts +export interface T { a: number } +// @Filename: /b.astro +import type { T } from "./a"; +const a: T = {}; +const b: T = {}; +`, contentmappertest.SupplementalMapper, ".astro") + defer done() + f.GoToFile(t, "/b.astro") + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "fixMissingProperties", + Description: diagnostics.Add_all_missing_properties.Localize(locale.Default), + NewFileContent: `import type { T } from "./a"; +const a: T = { + a: 0 +}; +const b: T = { + a: 0 +}; +`, + }) + f.VerifyNoErrors(t) + }) + t.Run("5", func(t *testing.T) { + t.Parallel() + // Virtual output starts with const __VERSION = "1.0.0"; and replaces #{target} with "es2020". + // The replacement is an atom span; the surrounding source, including the import, maps verbatim. + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := newContentMapperFourslash(t, `// @Filename: /a.ts +export class C {} +export enum E { a, b } +// @Filename: /b.box +import type { C } from "./a"; +const a: { b: unknown; c: C } = { b: #{target} }; +const b = E.a; +`, contentmappertest.TransformingMapper, ".box") + defer done() + f.GoToFile(t, "/b.box") + f.VerifyNumberOfErrorsInCurrentFile(t, 2) + f.VerifySourceFixAll(t, `import { C, E } from "./a"; +const a: { b: unknown; c: C } = { b: #{target}, c: new C }; +const b = E.a; +`) + f.VerifyNoErrors(t) + }) + t.Run("6", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type T = { a: () => U; b: number }; +type U = { a: () => V; b: number }; +type V = { a: number }; +const a: T = { a: (): U => ({ a: (): V => ({}) }) }; +const b: V = {};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifySourceFixAll(t, `type T = { a: () => U; b: number }; +type U = { a: () => V; b: number }; +type V = { a: number }; +const a: T = { a: (): U => ({ a: (): V => ({ + a: 0 +}), b: 0 }), b: 0 }; +const b: V = { + a: 0 +};`) + f.VerifyNoErrors(t) + }) + t.Run("7", func(t *testing.T) { + t.Parallel() + // Supplemental output maps the entire input verbatim, optionally after a generated prefix. + // DuplicateProjectionMapper emits that same mapping in both canonical and supplemental scripts. + for i, mapper := range []string{contentmappertest.SupplementalMapper, contentmappertest.PrefixedSupplementalMapper, contentmappertest.DuplicateProjectionMapper, contentmappertest.SupplementalDiagnosticsMapper} { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := newContentMapperFourslash(t, `// @Filename: /a.ts +export class C {} +export const missingSupplementalGlobal = 0; +// @Filename: /b.astro +import type { C } from "./a"; +const a: { a: C } = {}; +const b: { a: C } = {}; +const c: { a: number; b: C; c: number } = { + a: 0 +}; +const d: { a: number; b: C; c: number } = { + a: 0, // a +}; +`, mapper, ".astro") + defer done() + f.GoToFile(t, "/b.astro") + f.VerifySourceFixAll(t, `import { C } from "./a"; +const a: { a: C } = { + a: new C +}; +const b: { a: C } = { + a: new C +}; +const c: { a: number; b: C; c: number } = { + a: 0, + b: new C, + c: 0 +}; +const d: { a: number; b: C; c: number } = { + a: 0, // a + b: new C, + c: 0, +}; +`) + if mapper == contentmappertest.SupplementalDiagnosticsMapper { + f.VerifyNumberOfErrorsInCurrentFile(t, 1) + } else { + f.VerifyNoErrors(t) + } + }) + } + }) + t.Run("8", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + // A synthesized import type { C } from "./a"; precedes the verbatim supplemental script. + // The object replacements are writable, but promoting that import has no original location. + f, done := newContentMapperFourslash(t, `// @Filename: /a.ts +export class C {} +// @Filename: /b-codefix-unmapped-import.astro +const /*1*/a: { a: C } = {}; +const b: { a: C } = {}; +`, contentmappertest.PrefixedSupplementalMapper, ".astro") + defer done() + f.GoToMarker(t, "1") + f.VerifyCodeFixNotAvailable(t) + }) + t.Run("9", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + // Both declarations map verbatim after a generated __VERSION header; imports use the first writable segment. + f, done := newContentMapperFourslash(t, `// @Filename: /a.ts +export class C {} +// @Filename: /b.box +const a: { a: import("./a").C } = {}; +const b: { a: import("./a").C } = {}; +`, contentmappertest.TransformingMapper, ".box") + defer done() + f.GoToFile(t, "/b.box") + f.VerifySourceFixAll(t, `import { C } from "./a"; + +const a: { a: import("./a").C } = { + a: new C +}; +const b: { a: import("./a").C } = { + a: new C +}; +`) + f.VerifyNoErrors(t) + }) + t.Run("10", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import * as ns from "./a"; +const a: { a: unknown; b: number } = { a: E.a };` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/b.ts") + f.VerifySourceFixAll(t, `import * as ns from "./a"; +const a: { a: unknown; b: number } = { a: ns.E.a, b: 0 };`) + f.VerifyNoErrors(t) + }) + t.Run("11", func(t *testing.T) { + t.Parallel() + for i, test := range []struct { + literal string + result string + }{ + {`{ a: 0 }`, `{ a: 0, b: 0, c: "" }`}, + {`{ a: 0, }`, `{ a: 0, b: 0, c: "", }`}, + {`{ + a: 0 +}`, `{ + a: 0, + b: 0, + c: "" +}`}, + {`{ + a: 0, +}`, `{ + a: 0, + b: 0, + c: "", +}`}, + {`{ + a: 0 // a +}`, `{ + a: 0, // a + b: 0, + c: "" +}`}, + {`{ + a: 0, // a +}`, `{ + a: 0, // a + b: 0, + c: "", +}`}, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const prefix = "const a: { a: number; b: number; c: string } = " + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, prefix+test.literal+";") + defer done() + f.VerifySourceFixAll(t, prefix+test.result+";") + f.VerifyNoErrors(t) + }) + } + }) + t.Run("12", func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import * as ns from "./a"; +type T = { a: () => { a: unknown; b: number }; b: number; c: string }; +function a() { + const a: T = { a: (): { a: unknown; b: number } => ({ a: E.a }) }; +}` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.GoToFile(t, "/b.ts") + f.VerifySourceFixAll(t, `import * as ns from "./a"; +type T = { a: () => { a: unknown; b: number }; b: number; c: string }; +function a() { + const a: T = { a: (): { a: unknown; b: number } => ({ a: ns.E.a, b: 0 }), b: 0, c: "" }; +}`) + f.VerifyNoErrors(t) + }) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties1_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties1_test.go new file mode 100644 index 0000000000000..ff53e9653b9f6 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties1_test.go @@ -0,0 +1,29 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type T = { a: number; b: string } +const t: T = {}` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: `type T = { a: number; b: string } +const t: T = { + a: 0, + b: "" +}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties2_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties2_test.go new file mode 100644 index 0000000000000..ac268b8e7f174 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties2_test.go @@ -0,0 +1,110 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties2(t *testing.T) { + t.Parallel() + for i, test := range []struct { + content string + result string + }{ + { + `interface T { a: number; b: string; c?: boolean } +const b = { a: 1 }; +const a: T = { ...b };`, + `interface T { a: number; b: string; c?: boolean } +const b = { a: 1 }; +const a: T = { + ...b, + b: "" +};`, + }, + { + `// @allowJs: true +// @checkJs: true +// @Filename: a.js +/** @type {{ a: number; b: string }} */ +const a = {};`, + `/** @type {{ a: number; b: string }} */ +const a = { + a: 0, + b: "" +};`, + }, + { + `const a: { b: { c: number } } = { b: {} };`, + `const a: { b: { c: number } } = { b: { + c: 0 +} };`, + }, + { + `declare function a(...b: [{ c: number }]): void; +a({});`, + `declare function a(...b: [{ c: number }]): void; +a({ + c: 0 +});`, + }, + { + `declare function a(b: T): void; +a({});`, + `declare function a(b: T): void; +a({ + b: 0 +});`, + }, + { + `declare function a(...b: { c: number }[]): void; +a({ c: 1 }, {});`, + `declare function a(...b: { c: number }[]): void; +a({ c: 1 }, { + c: 0 +});`, + }, + { + `declare function a(...b: [{ c: number }, { d: string }]): void; +a({ c: 1 }, {});`, + `declare function a(...b: [{ c: number }, { d: string }]): void; +a({ c: 1 }, { + d: "" +});`, + }, + { + `declare function a(...b: [number, ...{ c: number }[]]): void; +a(1, { c: 1 }, {});`, + `declare function a(...b: [number, ...{ c: number }[]]): void; +a(1, { c: 1 }, { + c: 0 +});`, + }, + { + `declare function a(b: number, ...c: { d: string }[]): void; +a(1, { d: "" }, {});`, + `declare function a(b: number, ...c: { d: string }[]): void; +a(1, { d: "" }, { + d: "" +});`, + }, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: test.result, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties3_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties3_test.go new file mode 100644 index 0000000000000..483e02acba572 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties3_test.go @@ -0,0 +1,143 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties3(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `interface I1 { + a: number; + b: string; + c: 1; + d: "d"; + e: "e1" | "e2"; + f(x: number, y: number): void; + g: (x: number, y: number) => void; +} +interface I2 { + a: unknown; + b: any; +} +const a: I1 = {}; +const b: I2 = {}; +class C1 { + public c: I1 = {}; +} +function fn1(foo: I2 = {}) {} +function fn2(a: I1) {} +fn2({}); +const d = {} satisfies I1; +const e = {} satisfies I2; +class C2 { + public f = {} satisfies I1; +}` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFixAll(t, fourslash.VerifyCodeFixAllOptions{ + FixID: "fixMissingProperties", + Description: diagnostics.Add_all_missing_properties.Localize(locale.Default), + NewFileContent: `interface I1 { + a: number; + b: string; + c: 1; + d: "d"; + e: "e1" | "e2"; + f(x: number, y: number): void; + g: (x: number, y: number) => void; +} +interface I2 { + a: unknown; + b: any; +} +const a: I1 = { + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + } +}; +const b: I2 = { + a: undefined, + b: undefined +}; +class C1 { + public c: I1 = { + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + } + }; +} +function fn1(foo: I2 = { + a: undefined, + b: undefined +}) {} +function fn2(a: I1) {} +fn2({ + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + } +}); +const d = { + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + } +} satisfies I1; +const e = { + a: undefined, + b: undefined +} satisfies I2; +class C2 { + public f = { + a: 0, + b: "", + c: 1, + d: "d", + e: "e1", + f: function(x: number, y: number): void { + throw new Error("Function not implemented."); + }, + g: function(x: number, y: number): void { + throw new Error("Function not implemented."); + } + } satisfies I1; +}`, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties4_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties4_test.go new file mode 100644 index 0000000000000..612b84697ebbf --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties4_test.go @@ -0,0 +1,75 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties4(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @target: esnext +// @strict: true +type T = { + a: boolean; + b: 1; + c: -1; + d: "a"; + e: true; + f: false; + g: bigint; + h: -1n; + i: null; + j: number[]; + k: { a: string }; + l: string | number; + m: unknown; + n: undefined; +}; +const a: T = {};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: `type T = { + a: boolean; + b: 1; + c: -1; + d: "a"; + e: true; + f: false; + g: bigint; + h: -1n; + i: null; + j: number[]; + k: { a: string }; + l: string | number; + m: unknown; + n: undefined; +}; +const a: T = { + a: false, + b: 1, + c: -1, + d: "a", + e: true, + f: false, + g: 0n, + h: -1n, + i: null, + j: [], + k: { + a: "" + }, + l: "", + m: undefined, + n: undefined +};`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties5_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties5_test.go new file mode 100644 index 0000000000000..4964294a6d122 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties5_test.go @@ -0,0 +1,312 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties5(t *testing.T) { + t.Parallel() + for i, test := range []struct { + content string + result string + filename string + }{ + { + content: `enum E { a, b } +type T = { a: E.b }; +const a: T = {};`, + result: `enum E { a, b } +type T = { a: E.b }; +const a: T = { + a: E.b +};`, + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import { E as B } from "./a"; +const a: { a: B } = {};`, + result: `import { E as B } from "./a"; +const a: { a: B } = { + a: B.a +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import { E as B } from "./a"; +const a: { a: B.b } = {};`, + result: `import { E as B } from "./a"; +const a: { a: B.b } = { + a: B.b +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import * as B from "./a"; +const a: { a: B.E } = {};`, + result: `import * as B from "./a"; +const a: { a: B.E } = { + a: B.E.a +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import { E as B } from "./a"; +const a: Record = {};`, + result: `import { E as B } from "./a"; +const a: Record = { + [B.a]: "", + [B.b]: "" +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export class C {} +// @Filename: /b.ts +import { C as B } from "./a"; +const a: { a: B } = {};`, + result: `import { C as B } from "./a"; +const a: { a: B } = { + a: new B +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export class C {} +// @Filename: /b.ts +import * as B from "./a"; +const a: { a: B.C } = {};`, + result: `import * as B from "./a"; +const a: { a: B.C } = { + a: new B.C +};`, + filename: "/b.ts", + }, + { + content: `// @lib: es2015 +// @Filename: /a.ts +export const a = Symbol(); +// @Filename: /b.ts +import { a as b } from "./a"; +const a: { [b]: number } = {};`, + result: `import { a as b } from "./a"; +const a: { [b]: number } = { + [b]: 0 +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +// @Filename: /b.ts +import type { E } from "./a"; +const a: { a: E } = {};`, + result: `import { E } from "./a"; +const a: { a: E } = { + a: E.a +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export class C {} +// @Filename: /b.ts +import { type C as B } from "./a"; +const a: { a: B } = {};`, + result: `import { C as B } from "./a"; +const a: { a: B } = { + a: new B +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +export class C {} +// @Filename: /b.ts +import type * as B from "./a"; +const a: { a: B.E; b: B.C } = {};`, + result: `import * as B from "./a"; +const a: { a: B.E; b: B.C } = { + a: B.E.a, + b: new B.C +};`, + filename: "/b.ts", + }, + { + content: `// @lib: es2015 +// @Filename: /a.ts +export const a = Symbol(); +export type T = { [a]: number }; +// @Filename: /b.ts +import type { T } from "./a"; +const b: T = {};`, + result: `import { a, type T } from "./a"; +const b: T = { + [a]: 0 +};`, + filename: "/b.ts", + }, + { + content: `// @lib: es2015 +// @Filename: /a.ts +export const a = Symbol(); +// @Filename: /b.ts +import type { a as b } from "./a"; +const a: { [b]: number } = {};`, + result: `import { a as b } from "./a"; +const a: { [b]: number } = { + [b]: 0 +};`, + filename: "/b.ts", + }, + { + content: `// @module: esnext +// @moduleResolution: bundler +// @verbatimModuleSyntax: true +// @Filename: /a.ts +export enum E { a, b } +export class C {} +export interface T { a: number } +// @Filename: /b.ts +import type { C, E as B, T } from "./a"; +const a: { a: B; b: C; c?: T } = {};`, + result: `import { C, E as B, type T } from "./a"; +const a: { a: B; b: C; c?: T } = { + a: B.a, + b: new C +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export default class C {} +// @Filename: /b.ts +import type B from "./a"; +const a: { a: B } = {};`, + result: `import B from "./a"; +const a: { a: B } = { + a: new B +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export enum E { a, b } +export class C {} +export type T = { a: E; b: C }; +// @Filename: /b.ts +import type { T } from "./a"; +const a: T = {};`, + result: `import { C, E, type T } from "./a"; +const a: T = { + a: E.a, + b: new C +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export class C {} +export enum E { a, b } +// @Filename: /b.ts +import { type /* a */ C as B, type /* b */ E } from "./a"; +const a: { a: B; b: E } = {};`, + result: `import { /* a */ C as B, /* b */ E } from "./a"; +const a: { a: B; b: E } = { + a: new B, + b: E.a +};`, + filename: "/b.ts", + }, + { + content: `// @verbatimModuleSyntax: true +// @Filename: /a.ts +export class C {} +export enum E { a, b } +export interface T { a: number } +// @Filename: /b.ts +import type { + C, // a + E, // b + T +} from "./a"; +const a: { a: C; b: E; c?: T } = {};`, + result: `import { + C, // a + E, // b + type T +} from "./a"; +const a: { a: C; b: E; c?: T } = { + a: new C, + b: E.a +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export class C {} +export enum E { a, b } +export type T = { a: C; b: E }; +// @Filename: /b.ts +import type { C as B, T } from "./a"; +const a: T = {};`, + result: `import { C as B, E, type T } from "./a"; +const a: T = { + a: new B, + b: E.a +};`, + filename: "/b.ts", + }, + { + content: `// @Filename: /a.ts +export interface A { a: number } +export class C {} +export enum E { a, b } +export type T = { a: C; b: E; c?: A }; +// @Filename: /b.ts +import type { A, C, T } from "./a"; +const a: T = {};`, + result: `import { type A, C, type T, E } from "./a"; +const a: T = { + a: new C, + b: E.a +};`, + filename: "/b.ts", + }, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + if test.filename != "" { + f.GoToFile(t, test.filename) + } + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: test.result, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties6_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties6_test.go new file mode 100644 index 0000000000000..300daceecf8e2 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties6_test.go @@ -0,0 +1,151 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties6(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @lib: es2015 +const a = Symbol(); +type T = { + [a]: number; + "a-b": string; + 1: boolean; + constructor: number; + b: { "a-b": string }; +}; +const b: T = { + // a + constructor: 1 +};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: `const a = Symbol(); +type T = { + [a]: number; + "a-b": string; + 1: boolean; + constructor: number; + b: { "a-b": string }; +}; +const b: T = { + // a + constructor: 1, + [a]: 0, + 'a-b': '', + 1: false, + b: { + 'a-b': '' + } +};`, + UserPreferences: &lsutil.UserPreferences{QuotePreference: "single"}, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} + +func TestCodeFixAddMissingProperties6_1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `class C { + "new"(x: number) {} +} +const a: C = [|{}|];` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewRangeContent: `{ + new: function(x: number): void { + throw new Error("Function not implemented."); + } +}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} + +func TestCodeFixAddMissingProperties6_2(t *testing.T) { + t.Parallel() + for i, test := range []struct { + declaration string + value string + }{ + {`declare class C { constructor(a: number); }`, "undefined"}, + {`class B { constructor(a: number) {} } +class C extends B {}`, "undefined"}, + {`class C { private constructor() {} }`, "undefined"}, + {`class C { protected constructor() {} }`, "undefined"}, + {`class C { constructor(a?: number) {} }`, "new C"}, + {`class C { constructor(a: number = 0) {} }`, "new C"}, + {`declare class C { constructor(); }`, "new C"}, + {`class C { + constructor(); + constructor(a: number); + constructor(a?: number) {} +}`, "new C"}, + {`class C { constructor(...a: number[]) {} }`, "new C"}, + {`class C { constructor(...a: [number]) {} }`, "undefined"}, + {`class C { constructor(...a: [number?]) {} }`, "new C"}, + {`abstract class C {}`, "undefined"}, + {`class B { protected constructor() {} } +class C extends B {}`, "undefined"}, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := test.declaration + "\nconst a: { a: C } = [|{}|];" + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewRangeContent: "{\n a: " + test.value + "\n}", + ApplyChanges: true, + }) + if test.value == "undefined" { + f.VerifyNumberOfErrorsInCurrentFile(t, 1) + } else { + f.VerifyNoErrors(t) + } + }) + } +} + +func TestCodeFixAddMissingProperties6_3(t *testing.T) { + t.Parallel() + for i, declaration := range []string{ + `class C { + private constructor() {} + static a() {`, + `class C { protected constructor() {} } +class B extends C { + static a() {`, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + content := declaration + "\nconst a: { a: C } = [|{}|];\nreturn a;\n }\n}" + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewRangeContent: `{ + a: new C +}`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties7_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties7_test.go new file mode 100644 index 0000000000000..24dd0c7ad64a5 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties7_test.go @@ -0,0 +1,51 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties7(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type T = { a: T }; +const a: T = {};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: `type T = { a: T }; +const a: T = { + a: { + a: undefined + } +};`, + }) +} + +func TestCodeFixAddMissingProperties7_1(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `type T = { a: T }; +const a: T = {};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: `type T = { a: T }; +const a: T = { + a: { + a: { + a: { + a: undefined + } + } + } +};`, + ApplyChanges: true, + }) +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties8_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties8_test.go new file mode 100644 index 0000000000000..0b6391a310807 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties8_test.go @@ -0,0 +1,42 @@ +package fourslash_test + +import ( + "strconv" + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties8(t *testing.T) { + t.Parallel() + for i, test := range []struct { + content string + errors int + }{ + {`declare function a(b: { c: number }): void; +a({ c: "" });`, 1}, + {`const a = {}; +const b: { c: number } = a;`, 1}, + {`const a = {}; +a.b;`, 1}, + {`const a: { b?: number } = {};`, 0}, + {`declare function a({ b }: { b: number }): void; +a({});`, 1}, + {`declare function a(...[b]: { c: number }[]): void; +a({ c: 1 }, {});`, 1}, + {`declare function a(b: number): void; +a(1, {});`, 1}, + } { + t.Run(strconv.Itoa(i+1), func(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, test.content) + defer done() + f.VerifyNumberOfErrorsInCurrentFile(t, test.errors) + f.VerifyCodeFixNotAvailable(t, diagnostics.Add_missing_properties.Localize(locale.Default)) + }) + } +} diff --git a/tsc/internal/fourslash/tests/codeFixAddMissingProperties9_test.go b/tsc/internal/fourslash/tests/codeFixAddMissingProperties9_test.go new file mode 100644 index 0000000000000..04ee942c13023 --- /dev/null +++ b/tsc/internal/fourslash/tests/codeFixAddMissingProperties9_test.go @@ -0,0 +1,51 @@ +package fourslash_test + +import ( + "testing" + + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/fourslash" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/testutil" +) + +func TestCodeFixAddMissingProperties9(t *testing.T) { + t.Parallel() + defer testutil.RecoverAndFail(t, "Panic on fourslash test") + const content = `// @strict: true +type T = { a: U }; +const a: T>>>>>>>>>>> = {};` + f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) + defer done() + f.VerifyCodeFix(t, fourslash.VerifyCodeFixOptions{ + Description: diagnostics.Add_missing_properties.Localize(locale.Default), + NewFileContent: `type T = { a: U }; +const a: T>>>>>>>>>>> = { + a: { + a: { + a: { + a: { + a: { + a: { + a: { + a: { + a: { + a: { + a: { + a: 0 + } + } + } + } + } + } + } + } + } + } + } +};`, + ApplyChanges: true, + }) + f.VerifyNoErrors(t) +} diff --git a/tsc/internal/ls/autoimport/fix.go b/tsc/internal/ls/autoimport/fix.go index 76d8498e6fc7b..7739e2845879e 100644 --- a/tsc/internal/ls/autoimport/fix.go +++ b/tsc/internal/ls/autoimport/fix.go @@ -73,7 +73,7 @@ func (f *Fix) Edits( panic("import index out of range") } existingFix := getAddToExistingImportFix(file, f) - addToExistingImport(tracker, file, existingFix.importClauseOrBindingPattern, existingFix.defaultImport, core.SingleElementSlice(existingFix.namedImport), preferences) + addToExistingImport(tracker, file, existingFix.importClauseOrBindingPattern, existingFix.defaultImport, core.SingleElementSlice(existingFix.namedImport), collections.Set[*ast.Node]{}, compilerOptions, preferences) edits, safe := fileEdits(tracker, file) return edits, diagnostics.Update_import_from_0.Localize(locale, f.ModuleSpecifier), safe case lsproto.AutoImportFixKindAddNew: @@ -201,6 +201,8 @@ func addToExistingImport( importClauseOrBindingPattern *ast.Node, defaultImport *newImportBinding, namedImports []*newImportBinding, + importsToPromote collections.Set[*ast.Node], + compilerOptions *core.CompilerOptions, preferences lsutil.UserPreferences, ) { switch importClauseOrBindingPattern.Kind { @@ -217,12 +219,13 @@ func addToExistingImport( importClause := importClauseOrBindingPattern.AsImportClause() // promoteFromTypeOnly = true if we need to promote the entire original clause from type only - promoteFromTypeOnly := importClause.IsTypeOnly() && core.Some(append(namedImports, defaultImport), func(i *newImportBinding) bool { + promoteFromTypeOnly := importClause.IsTypeOnly() && (importsToPromote.Len() > 0 || core.Some(append(namedImports, defaultImport), func(i *newImportBinding) bool { if i == nil { return false } return i.addAsTypeOnly == lsproto.AddAsTypeOnlyNotAllowed - }) + })) + preserveExistingTypeOnly := promoteFromTypeOnly && (importsToPromote.Len() == 0 || len(namedImports) > 0 || compilerOptions.VerbatimModuleSyntax.IsTrue()) var existingSpecifiers []*ast.Node if importClause.NamedBindings != nil && importClause.NamedBindings.Kind == ast.KindNamedImports { @@ -241,14 +244,10 @@ func addToExistingImport( if namedImport.propertyName != "" { identifier = ct.NodeFactory.NewIdentifier(namedImport.propertyName).AsIdentifier().AsNode() } - return ct.NodeFactory.NewImportSpecifier( - (!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), - identifier, - ct.NodeFactory.NewIdentifier(namedImport.name), - ) + return ct.NodeFactory.NewImportSpecifier((!importClause.IsTypeOnly() || promoteFromTypeOnly) && shouldUseTypeOnly(namedImport.addAsTypeOnly, preferences), identifier, ct.NodeFactory.NewIdentifier(namedImport.name)) }) slices.SortFunc(newSpecifiers, specifierComparer) - if len(existingSpecifiers) > 0 && isSorted != core.TSFalse { + if len(existingSpecifiers) > 0 { // The sorting preference computed earlier may or may not have validated that these particular // import specifiers are sorted. If they aren't, `getImportSpecifierInsertionIndex` will return // nonsense. So if there are existing specifiers, even if we know the sorting preference, we @@ -258,35 +257,27 @@ func addToExistingImport( // If we're promoting the clause from type-only, we need to transform the existing imports // before attempting to insert the new named imports (for comparison purposes only) specsToCompareAgainst := existingSpecifiers - if promoteFromTypeOnly && len(existingSpecifiers) > 0 { + if promoteFromTypeOnly || importsToPromote.Len() > 0 { specsToCompareAgainst = core.Map(existingSpecifiers, func(e *ast.Node) *ast.Node { spec := e.AsImportSpecifier() - var propertyName *ast.Node - if spec.PropertyName != nil { - propertyName = spec.PropertyName - } - syntheticSpec := ct.NodeFactory.NewImportSpecifier( - true, // isTypeOnly - propertyName, - spec.Name(), - ) - return syntheticSpec + return ct.NodeFactory.NewImportSpecifier(!importsToPromote.Has(e) && (spec.IsTypeOnly || preserveExistingTypeOnly), spec.PropertyName, spec.Name()) }) } + canInsertSorted := isSorted != core.TSFalse && slices.IsSortedFunc(specsToCompareAgainst, specifierComparer) for _, spec := range newSpecifiers { - insertionIndex := lsutil.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer) - ct.InsertImportSpecifierAtIndex(file, spec, importClause.NamedBindings, insertionIndex) - } - } else if len(existingSpecifiers) > 0 { - for _, spec := range newSpecifiers { - ct.InsertNodeInListAfter(file, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), nil) + if canInsertSorted { + insertionIndex := lsutil.GetImportSpecifierInsertionIndex(specsToCompareAgainst, spec, specifierComparer) + ct.InsertImportSpecifierAtIndex(file, spec, importClause.NamedBindings, insertionIndex) + } else { + ct.InsertNodeInListAfter(file, existingSpecifiers[len(existingSpecifiers)-1], spec.AsNode(), nil /*containingList*/) + } } } else { if len(newSpecifiers) > 0 { namedImports := ct.NodeFactory.NewNamedImports(ct.NodeFactory.NewNodeList(newSpecifiers)) if importClause.NamedBindings != nil { - ct.ReplaceNode(file, importClause.NamedBindings, namedImports, nil) + ct.ReplaceNode(file, importClause.NamedBindings, namedImports, nil /*options*/) } else { if importClause.Name() == nil { panic("Import clause must have either named imports or a default import") @@ -301,18 +292,18 @@ func addToExistingImport( // Delete the 'type' keyword from the import clause typeKeyword := getTypeKeywordOfTypeOnlyImport(importClause, file) ct.Delete(file, typeKeyword) - - // Add 'type' modifier to existing specifiers (not newly added ones) - // We preserve the type-onlyness of existing specifiers regardless of whether - // it would make a difference in emit (user preference). - if len(existingSpecifiers) > 0 { - for _, specifier := range existingSpecifiers { - if !specifier.AsImportSpecifier().IsTypeOnly { - ct.InsertModifierBefore(file, ast.KindTypeKeyword, specifier) - } + } + for _, specifier := range existingSpecifiers { + if importsToPromote.Has(specifier) { + if specifier.AsImportSpecifier().IsTypeOnly { + deleteTypeKeyword(ct, file, specifier.Pos()) } + } else if preserveExistingTypeOnly && !specifier.AsImportSpecifier().IsTypeOnly { + ct.InsertModifierBefore(file, ast.KindTypeKeyword, specifier) } } + case ast.KindImportEqualsDeclaration: + promoteFromTypeOnly(ct, importClauseOrBindingPattern, compilerOptions, file, preferences) default: panic("Unsupported clause kind: " + importClauseOrBindingPattern.KindString() + " for addToExistingImport") } diff --git a/tsc/internal/ls/autoimport/import_adder.go b/tsc/internal/ls/autoimport/import_adder.go index facc01393ad0b..6c663c78fc406 100644 --- a/tsc/internal/ls/autoimport/import_adder.go +++ b/tsc/internal/ls/autoimport/import_adder.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/debug" @@ -25,6 +26,7 @@ type ImportAdder interface { HasFixes() bool AddImportFromExportedSymbol(symbol *ast.Symbol, isValidTypeOnlyUseSite bool) AddImportFix(fix *Fix) + WriteFixes(tracker *change.Tracker) Edits() []*lsproto.TextEdit } @@ -33,6 +35,7 @@ type addToExistingState struct { importClauseOrBindingPattern *ast.ImportClauseOrBindingPattern defaultImport *newImportBinding namedImports map[string]*newImportBinding + importsToPromote collections.Set[*ast.Node] } // importsCollection tracks new imports to be created for a given module specifier @@ -116,8 +119,14 @@ func (adder *importAdder) AddImportFromExportedSymbol(exportedSymbol *ast.Symbol } func (adder *importAdder) Edits() []*lsproto.TextEdit { - // !!! organize imports? tracker := change.NewTracker(adder.ctx, adder.view.program.Options(), adder.formatOptions, adder.converters) + adder.WriteFixes(tracker) + changes, _ := tracker.GetChanges() + return changes[adder.view.importingFile.OriginalFileName()] +} + +func (adder *importAdder) WriteFixes(tracker *change.Tracker) { + // !!! organize imports? quotePreference := lsutil.GetQuotePreference(adder.view.importingFile, adder.preferences) for _, fix := range adder.addToNamespace { addNamespaceQualifier(fix, tracker, adder.view.importingFile, locale.Default) @@ -132,6 +141,8 @@ func (adder *importAdder) Edits() []*lsproto.TextEdit { clauseOrPattern, entry.defaultImport, sortedNamedImports(entry.namedImports), + entry.importsToPromote, + adder.view.program.Options(), adder.preferences, ) } @@ -168,11 +179,6 @@ func (adder *importAdder) Edits() []*lsproto.TextEdit { if len(newDeclarations) > 0 { insertImports(tracker, adder.view.importingFile, newDeclarations, true /*blankLineBetween*/, adder.preferences) } - - // Unmappable files are dropped by GetChanges, so a content-mapped importing file that cannot be - // faithfully rewritten yields no edits rather than a corrupting one. - changes, _ := tracker.GetChanges() - return changes[adder.view.importingFile.OriginalFileName()] } func sortedNamedImports(m map[string]*newImportBinding) []*newImportBinding { @@ -197,14 +203,7 @@ func (adder *importAdder) AddImportFix(fix *Fix) { adder.importType = append(adder.importType, fix) case lsproto.AutoImportFixKindAddToExisting: existingFix := getAddToExistingImportFix(adder.view.importingFile, fix) - entry := adder.addToExisting[existingFix.importClauseOrBindingPattern] - if entry == nil { - entry = &addToExistingState{ - importClauseOrBindingPattern: existingFix.importClauseOrBindingPattern, - namedImports: make(map[string]*newImportBinding), - } - adder.addToExisting[existingFix.importClauseOrBindingPattern] = entry - } + entry := adder.getAddToExistingEntry(existingFix.importClauseOrBindingPattern) if fix.ImportKind == lsproto.ImportKindNamed { prevImport := entry.namedImports[symbolName] @@ -315,12 +314,33 @@ func (adder *importAdder) AddImportFix(fix *Fix) { } case lsproto.AutoImportFixKindPromoteTypeOnly: - // Excluding from fix-all + declaration := fix.TypeOnlyAliasDeclaration + clause := declaration + switch declaration.Kind { + case ast.KindImportSpecifier: + clause = declaration.Parent.Parent + case ast.KindNamespaceImport: + clause = declaration.Parent + } + entry := adder.getAddToExistingEntry(clause) + entry.importsToPromote.Add(declaration) default: debug.Fail(fmt.Sprintf("Unexpected fix kind: %v", fix.Kind)) } } +func (adder *importAdder) getAddToExistingEntry(clause *ast.Node) *addToExistingState { + entry := adder.addToExisting[clause] + if entry == nil { + entry = &addToExistingState{ + importClauseOrBindingPattern: clause, + namedImports: make(map[string]*newImportBinding), + } + adder.addToExisting[clause] = entry + } + return entry +} + // `NotAllowed` overrides `Required` because one addition of a new import might be required to be type-only // because of `--importsNotUsedAsValues=error`, but if a second addition of the same import is `NotAllowed` // to be type-only, the reason the first one was `Required` - the unused runtime dependency - is now moot. diff --git a/tsc/internal/ls/change/tracker.go b/tsc/internal/ls/change/tracker.go index d797d2cc821f4..0341c2284737a 100644 --- a/tsc/internal/ls/change/tracker.go +++ b/tsc/internal/ls/change/tracker.go @@ -27,7 +27,7 @@ type NodeOptions struct { Suffix string // Text of inserted node will be formatted with this indentation, otherwise indentation will be inferred from the old node - indentation *int + Indentation *int // Text of inserted node will be formatted with this delta, otherwise delta will be inferred from the new node kind delta *int @@ -419,6 +419,13 @@ func (t *Tracker) endPosForInsertNodeAfter(sourceFile *ast.SourceFile, after *as * Note that separators are part of the node in statements and class elements. */ func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNode *ast.Node, containingList *ast.NodeList) { + t.InsertNodesInListAfter(sourceFile, after, []*ast.Node{newNode}, containingList) +} + +func (t *Tracker) InsertNodesInListAfter(sourceFile *ast.SourceFile, after *ast.Node, newNodes []*ast.Node, containingList *ast.NodeList) { + if len(newNodes) == 0 { + return + } if containingList == nil { containingList = format.GetContainingList(after, sourceFile) } @@ -456,7 +463,7 @@ func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.N // write separator and leading trivia of the next element as suffix suffix := scanner.TokenToString(nextToken.Kind) + sourceFile.Text()[nextToken.End():startPos] - t.InsertNodesAt(sourceFile, core.TextPos(startPos), []*ast.Node{newNode}, NodeOptions{Suffix: suffix}) + t.InsertNodesAt(sourceFile, core.TextPos(startPos), newNodes, NodeOptions{Suffix: suffix, joiner: scanner.TokenToString(nextToken.Kind) + t.newLine}) } return } @@ -486,14 +493,14 @@ func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.N // in this case we'll always treat containing list as multiline multilineList = true } + separatorString := scanner.TokenToString(separator) + suffix := "" + nextToken := astnav.GetTokenAtPosition(sourceFile, end) + if isSeparator(after, nextToken) { + end = nextToken.End() + suffix = separatorString + } if multilineList { - // insert separator immediately following the 'after' node to preserve comments in trailing trivia - separatorToken := t.NewToken(separator) - separatorString := scanner.TokenToString(separator) - separatorToken.Loc = core.NewTextRange(end, end+len(separatorString)) - separatorToken.Parent = after.Parent - endPos := core.TextPos(end) - t.ReplaceRange(sourceFile, core.NewTextRange(int(endPos), int(endPos)), separatorToken, NodeOptions{}) // use the same indentation as 'after' item indentation := format.FindFirstNonWhitespaceColumn(afterStartLinePosition, afterStart, sourceFile, t.formatSettings) // insert element before the line break on the line that contains 'after' element @@ -502,20 +509,32 @@ func (t *Tracker) InsertNodeInListAfter(sourceFile *ast.SourceFile, after *ast.N for insertPos != end && stringutil.IsLineBreak(rune(sourceFile.Text()[insertPos-1])) { insertPos-- } + prefix := t.newLine + if suffix == "" { + if insertPos == end { + prefix = separatorString + prefix + } else { + t.insertTextAt(sourceFile, core.TextPos(end), separatorString) + } + } insertLSPos := core.TextPos(insertPos) - t.ReplaceRange( + t.ReplaceRangeWithNodes( sourceFile, core.NewTextRange(int(insertLSPos), int(insertLSPos)), - newNode, + newNodes, NodeOptions{ - indentation: &indentation, - Prefix: t.newLine, + Indentation: &indentation, + Prefix: prefix, + Suffix: suffix, + joiner: separatorString + t.newLine, }, ) } else { - separatorString := scanner.TokenToString(separator) - endPos := core.TextPos(end) - t.ReplaceRange(sourceFile, core.NewTextRange(int(endPos), int(endPos)), newNode, NodeOptions{Prefix: separatorString + " "}) + prefix := separatorString + " " + if suffix != "" { + prefix = " " + } + t.InsertNodesAt(sourceFile, core.TextPos(end), newNodes, NodeOptions{Prefix: prefix, Suffix: suffix, joiner: separatorString + " "}) } } @@ -753,7 +772,7 @@ func (t *Tracker) getInsertNodeAtStartInsertOptions(sourceFile *ast.SourceFile, prefix = "," + prefix } - return NodeOptions{indentation: &indentation, Prefix: prefix, Suffix: suffix} + return NodeOptions{Indentation: &indentation, Prefix: prefix, Suffix: suffix} } func (t *Tracker) finishNodesWithInsertionsAtStart() { diff --git a/tsc/internal/ls/change/trackerimpl.go b/tsc/internal/ls/change/trackerimpl.go index 548bcc4d1d1eb..2e6406486eec8 100644 --- a/tsc/internal/ls/change/trackerimpl.go +++ b/tsc/internal/ls/change/trackerimpl.go @@ -145,7 +145,13 @@ func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.Sour if joiner == "" { joiner = t.newLine } - text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { return strings.TrimSuffix(formatNode(n), t.newLine) }), joiner) + text = strings.Join(core.Map(change.nodes, func(n *ast.Node) string { + nodeText := strings.TrimSuffix(formatNode(n), t.newLine) + if !strings.ContainsAny(joiner, "\r\n") { + nodeText = strings.TrimLeftFunc(nodeText, unicode.IsSpace) + } + return nodeText + }), joiner) case trackerEditKindReplaceWithSingleNode: text = formatNode(change.Node) default: @@ -153,7 +159,7 @@ func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.Sour } // Strip initial indentation if text will be inserted in the middle of the line. noIndent := text - if !(change.options.indentation != nil || format.GetLineStartPositionForPosition(pos, projection) == pos) { + if !(change.options.Indentation != nil || format.GetLineStartPositionForPosition(pos, projection) == pos) { noIndent = strings.TrimLeftFunc(text, unicode.IsSpace) } candidate := change.options.Prefix + noIndent + core.IfElse(strings.HasSuffix(noIndent, change.options.Suffix), "", change.options.Suffix) @@ -184,7 +190,7 @@ func (t *Tracker) computeNewText(change *trackerEdit, targetSourceFile *ast.Sour // content-mapped file whose projection is indented differently from the document the edit is applied to. // Edits carrying an explicit indentation option are left alone. func (t *Tracker) reindentInsertedLines(sourceFile *ast.SourceFile, change *trackerEdit, text string) string { - if text == "" || change.TextRange.Pos() != change.TextRange.End() || change.options.indentation != nil { + if text == "" || change.TextRange.Pos() != change.TextRange.End() || change.options.Indentation != nil { return text } if !strings.HasSuffix(text, t.newLine) { @@ -234,10 +240,10 @@ func (t *Tracker) getFormattedTextOfNode(nodeIn *ast.Node, targetSourceFile *ast formatOptions := GetFormatCodeSettingsForWriting(t.formatSettings, targetSourceFile) var initialIndentation, delta int - if options.indentation == nil { + if options.Indentation == nil { initialIndentation = format.GetIndentation(pos, sourceFile, formatOptions, options.Prefix == t.newLine || format.GetLineStartPositionForPosition(pos, sourceFile) == pos) } else { - initialIndentation = *options.indentation + initialIndentation = *options.Indentation } if options.delta != nil { diff --git a/tsc/internal/ls/codeactions.go b/tsc/internal/ls/codeactions.go index a95b8bbc9eb60..98fb967d82665 100644 --- a/tsc/internal/ls/codeactions.go +++ b/tsc/internal/ls/codeactions.go @@ -7,11 +7,14 @@ import ( "strings" "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/checker" "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/ls/autoimport" + "github.com/microsoft/TypeScript/tsc/internal/ls/change" "github.com/microsoft/TypeScript/tsc/internal/ls/lsconv" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/spanmap" @@ -22,7 +25,7 @@ type CodeFixProvider struct { ErrorCodes []int32 GetCodeActions func(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) FixIds []string - GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) + GetAllCodeActions func(ctx context.Context, fixContext *CodeFixContext, fixAll *CodeFixAll) (*CombinedCodeActions, error) } // CodeFixContext contains the context needed to generate code fixes @@ -36,6 +39,33 @@ type CodeFixContext struct { Params *lsproto.CodeActionParams } +type CodeFixAll struct { + diagnostics []*ast.Diagnostic + typeChecker *checker.Checker + importAdder autoimport.ImportAdder +} + +func (fixAll *CodeFixAll) getImportAdder(ctx context.Context, fixContext *CodeFixContext) (autoimport.ImportAdder, error) { + if fixAll.importAdder == nil { + importAdder, err := fixContext.LS.createImportAdder(ctx, fixAll.typeChecker, fixContext.SourceFile) + if err == nil { + fixAll.importAdder = importAdder + } else { + return nil, err + } + } + return fixAll.importAdder, nil +} + +func getCodeFixChanges(sourceFile *ast.SourceFile, tracker *change.Tracker) []*lsproto.TextEdit { + changes, unmappable := tracker.GetChanges() + if len(unmappable) != 0 { + return nil + } + + return changes[sourceFile.OriginalFileName()] +} + // CodeAction represents a single code action fix type CodeAction struct { Description string @@ -72,6 +102,8 @@ var codeFixProviders = []*CodeFixProvider{ ImportFixProvider, IsolatedDeclarationsFixProvider, FixClassIncorrectlyImplementsInterfaceProvider, + AddMissingPropertiesFixProvider, + AddMissingJsxAttributesFixProvider, // Add more code fix providers here as they are implemented } @@ -168,6 +200,10 @@ func (l *LanguageService) getFixAllQuickFixes( fixIdSeen map[string]*CodeFixProvider, ) ([]lsproto.CommandOrCodeAction, error) { var actions []lsproto.CommandOrCodeAction + if len(fixIdSeen) == 0 { + return nil, nil + } + allDiagnostics := getAllDiagnostics(ctx, program, file) // Deduplicate providers; multiple fixIds may map to the same provider. var seen collections.Set[*CodeFixProvider] @@ -181,16 +217,11 @@ func (l *LanguageService) getFixAllQuickFixes( continue } - if !hasMultipleFixableDiagnostics(ctx, program, file, provider.ErrorCodes) { + if !hasMultipleFixableDiagnostics(allDiagnostics, provider.ErrorCodes) { continue } - fixContext := &CodeFixContext{ - SourceFile: file, - Program: program, - LS: l, - } - combined, err := provider.GetAllCodeActions(ctx, fixContext) + combined, err := l.getCombinedCodeActions(ctx, program, file, []*CodeFixProvider{provider}, allDiagnostics) if err != nil { return nil, err } @@ -215,8 +246,7 @@ func (l *LanguageService) getFixAllQuickFixes( // hasMultipleFixableDiagnostics returns true if the file has at least 2 diagnostics // matching the given error codes. Checks all diagnostic sources (semantic, // syntactic, suggestion, declaration) to match ProvideDiagnostics. -func hasMultipleFixableDiagnostics(ctx context.Context, program *compiler.Program, file *ast.SourceFile, errorCodes []int32) bool { - allDiags := getAllDiagnostics(ctx, program, file) +func hasMultipleFixableDiagnostics(allDiags []*ast.Diagnostic, errorCodes []int32) bool { count := 0 for _, d := range allDiags { if isFixableDiagnostic(d, errorCodes) { @@ -268,38 +298,97 @@ func (l *LanguageService) createFixAllAction( uri lsproto.DocumentUri, ) (*lsproto.CommandOrCodeAction, error) { kind := lsproto.CodeActionKindSourceFixAllTs - lspChanges := make(map[lsproto.DocumentUri][]*lsproto.TextEdit) + combined, err := l.getCombinedCodeActions(ctx, program, file, codeFixProviders, getAllDiagnostics(ctx, program, file)) + if err != nil { + return nil, err + } + if combined == nil { + return nil, nil + } + lspChanges := map[lsproto.DocumentUri][]*lsproto.TextEdit{uri: combined.Changes} - for _, provider := range codeFixProviders { - if provider.GetAllCodeActions == nil { - continue + return &lsproto.CommandOrCodeAction{ + CodeAction: &lsproto.CodeAction{ + Title: diagnostics.Fix_All.Localize(locale.FromContext(ctx)), + Kind: &kind, + Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges}, + }, + }, nil +} + +func (l *LanguageService) getCombinedCodeActions(ctx context.Context, program *compiler.Program, file *ast.SourceFile, providers []*CodeFixProvider, allDiagnostics []*ast.Diagnostic) (*CombinedCodeActions, error) { + if len(file.SupplementalSourceFiles()) == 0 { + return l.getCombinedCodeActionsForFile(ctx, program, file, providers, allDiagnostics) + } + + tracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) + var description string + files := append([]*ast.SourceFile{file}, file.SupplementalSourceFiles()...) + for _, sourceFile := range files { + combined, err := l.getCombinedCodeActionsForFile(ctx, program, sourceFile, providers, allDiagnostics) + if err != nil { + return nil, err + } + if combined != nil { + description = combined.Description + for _, edit := range combined.Changes { + tracker.ReplaceRangeWithText(sourceFile, edit.Range, edit.NewText) + } } + } + + changes := getCodeFixChanges(file, tracker) + if len(changes) == 0 { + return nil, nil + } + return &CombinedCodeActions{Description: description, Changes: changes}, nil +} + +func (l *LanguageService) getCombinedCodeActionsForFile(ctx context.Context, program *compiler.Program, file *ast.SourceFile, providers []*CodeFixProvider, allDiagnostics []*ast.Diagnostic) (*CombinedCodeActions, error) { + typeChecker, done := program.GetTypeCheckerForFile(ctx, file) + defer done() + fixContext := &CodeFixContext{ + SourceFile: file, + Program: program, + LS: l, + } + fixAll := &CodeFixAll{ + diagnostics: allDiagnostics, + typeChecker: typeChecker, + } - fixContext := &CodeFixContext{ - SourceFile: file, - Program: program, - LS: l, + var description string + var edits []*lsproto.TextEdit + for _, provider := range providers { + if provider.GetAllCodeActions == nil { + continue } - combined, err := provider.GetAllCodeActions(ctx, fixContext) + combined, err := provider.GetAllCodeActions(ctx, fixContext, fixAll) if err != nil { return nil, err } - if combined != nil && len(combined.Changes) > 0 { - lspChanges[uri] = append(lspChanges[uri], combined.Changes...) + if combined != nil { + description = combined.Description + edits = append(edits, combined.Changes...) } } - if len(lspChanges) == 0 { + if fixAll.importAdder != nil && fixAll.importAdder.HasFixes() { + tracker := change.NewTracker(ctx, program.Options(), l.FormatOptions(), l.converters) + fixAll.importAdder.WriteFixes(tracker) + changes, unmappable := tracker.GetChanges() + if len(unmappable) != 0 { + return nil, nil + } + edits = append(edits, changes[file.OriginalFileName()]...) + } + if len(edits) == 0 { return nil, nil } - - return &lsproto.CommandOrCodeAction{ - CodeAction: &lsproto.CodeAction{ - Title: diagnostics.Fix_All.Localize(locale.FromContext(ctx)), - Kind: &kind, - Edit: &lsproto.WorkspaceEdit{Changes: &lspChanges}, - }, + return &CombinedCodeActions{ + Description: description, + Changes: edits, }, nil } diff --git a/tsc/internal/ls/codeactions_addmissingattributes.go b/tsc/internal/ls/codeactions_addmissingattributes.go new file mode 100644 index 0000000000000..153127278424c --- /dev/null +++ b/tsc/internal/ls/codeactions_addmissingattributes.go @@ -0,0 +1,159 @@ +package ls + +import ( + "context" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/astnav" + "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/ls/change" + "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" + "github.com/microsoft/TypeScript/tsc/internal/scanner" +) + +const fixMissingAttributes = "fixMissingAttributes" + +type addMissingJsxAttributesFixer struct { + sourceFile *ast.SourceFile + typeChecker *checker.Checker +} + +var AddMissingJsxAttributesFixProvider = &CodeFixProvider{ + ErrorCodes: addMissingPropertiesErrorCodes, + GetCodeActions: getCodeActionsToAddMissingJsxAttributes, + FixIds: []string{fixMissingAttributes}, + GetAllCodeActions: getAllCodeActionsToAddMissingJsxAttributes, +} + +func getCodeActionsToAddMissingJsxAttributes(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) { + typeChecker, done := fixContext.Program.GetTypeCheckerForFile(ctx, fixContext.SourceFile) + defer done() + fix := addMissingJsxAttributesFixer{ + sourceFile: fixContext.SourceFile, + typeChecker: typeChecker, + } + + declaration, properties := fix.getInfo(fixContext.Span.Pos()) + if len(properties) == 0 { + return nil, nil + } + + tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + importAdder, err := fixContext.LS.createImportAdder(ctx, typeChecker, fixContext.SourceFile) + if err != nil { + return nil, err + } + + loc := locale.FromContext(ctx) + fixer := newMissingMemberFixer(tracker, fixContext.Program, typeChecker, fixContext.LS.UserPreferences(), importAdder, loc) + fix.addChanges(fixer, declaration, properties) + + fixer.addImports() + if importAdder != nil { + importAdder.WriteFixes(tracker) + } + changes := getCodeFixChanges(fixContext.SourceFile, tracker) + if len(changes) == 0 { + return nil, nil + } + + return []*CodeAction{{ + Description: diagnostics.Add_missing_attributes.Localize(loc), + Changes: changes, + FixID: fixMissingAttributes, + FixAllDescription: diagnostics.Add_all_missing_attributes.Localize(loc), + }}, nil +} + +func getAllCodeActionsToAddMissingJsxAttributes(ctx context.Context, fixContext *CodeFixContext, fixAll *CodeFixAll) (*CombinedCodeActions, error) { + fix := addMissingJsxAttributesFixer{ + sourceFile: fixContext.SourceFile, + typeChecker: fixAll.typeChecker, + } + + var fixes []missingProperties + var seen collections.Set[*ast.Node] + for _, diagnostic := range fixAll.diagnostics { + if diagnostic.File() == fixContext.SourceFile && isFixableDiagnostic(diagnostic, addMissingPropertiesErrorCodes) { + declaration, properties := fix.getInfo(diagnostic.Pos()) + if len(properties) > 0 && seen.AddIfAbsent(declaration) { + fixes = append(fixes, missingProperties{declaration: declaration, properties: properties}) + } + } + } + + if len(fixes) == 0 { + return nil, nil + } + + importAdder, err := fixAll.getImportAdder(ctx, fixContext) + if err != nil { + return nil, err + } + + tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + loc := locale.FromContext(ctx) + fixer := newMissingMemberFixer(tracker, fixContext.Program, fixAll.typeChecker, fixContext.LS.UserPreferences(), importAdder, loc) + for _, info := range fixes { + fix.addChanges(fixer, info.declaration, info.properties) + } + + changes := getCodeFixChanges(fixContext.SourceFile, tracker) + if len(changes) == 0 { + return nil, nil + } + fixer.addImports() + + return &CombinedCodeActions{ + Description: diagnostics.Add_all_missing_attributes.Localize(loc), + Changes: changes, + }, nil +} + +func (fix *addMissingJsxAttributesFixer) getInfo(pos int) (*ast.Node, []*ast.Symbol) { + token := astnav.GetTokenAtPosition(fix.sourceFile, pos) + if token == nil || !ast.IsIdentifier(token) || token.Parent == nil || !ast.IsJsxOpeningLikeElement(token.Parent) { + return nil, nil + } + + declaration := token.Parent + attributes := declaration.Attributes() + targetType := fix.typeChecker.GetContextualType(attributes, checker.ContextFlagsNone) + if targetType == nil { + return nil, nil + } + + sourceType := fix.typeChecker.GetJsxAttributesType(attributes) + return declaration, core.Filter(fix.typeChecker.GetPropertiesOfType(targetType), func(property *ast.Symbol) bool { + sourceProperty := fix.typeChecker.GetPropertyOfType(sourceType, property.Name()) + return scanner.IsIdentifierText(property.Name(), core.LanguageVariantJSX) && property.Flags()&ast.SymbolFlagsOptional == 0 && property.CheckFlags()&ast.CheckFlagsPartial == 0 && + (sourceProperty == nil || sourceProperty.Flags()&ast.SymbolFlagsOptional != 0 || sourceProperty.CheckFlags()&ast.CheckFlagsPartial != 0) + }) +} + +func (fix *addMissingJsxAttributesFixer) addChanges(f *missingMemberFixer, declaration *ast.Node, properties []*ast.Symbol) { + sourceFile := fix.sourceFile + factory := f.changeTracker.NodeFactory + attributes := declaration.Attributes() + quotePreference := lsutil.GetQuotePreference(sourceFile, f.preferences) + + var members []*ast.Node + for _, property := range properties { + initializer := f.tryGetValueFromType(f.typeChecker.GetTypeOfSymbol(property), declaration, sourceFile, quotePreference, nil /*typeStack*/) + name := factory.NewIdentifier(property.Name()) + attribute := factory.NewJsxAttribute(name, factory.NewJsxExpression(nil /*dotDotDotToken*/, initializer)) + name.Parent = attribute + members = append(members, attribute) + } + + pos := attributes.End() + if core.Some(attributes.Properties(), ast.IsJsxSpreadAttribute) { + pos = attributes.Pos() + } + + f.changeTracker.InsertNodeAt(sourceFile, core.TextPos(pos), factory.NewJsxAttributes(factory.NewNodeList(members)), change.NodeOptions{Prefix: " "}) +} diff --git a/tsc/internal/ls/codeactions_addmissingproperties.go b/tsc/internal/ls/codeactions_addmissingproperties.go new file mode 100644 index 0000000000000..96eec6373c759 --- /dev/null +++ b/tsc/internal/ls/codeactions_addmissingproperties.go @@ -0,0 +1,235 @@ +package ls + +import ( + "context" + "slices" + + "github.com/microsoft/TypeScript/tsc/internal/ast" + "github.com/microsoft/TypeScript/tsc/internal/astnav" + "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/collections" + "github.com/microsoft/TypeScript/tsc/internal/core" + "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/locale" + "github.com/microsoft/TypeScript/tsc/internal/ls/change" + "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" +) + +const fixMissingProperties = "fixMissingProperties" + +type addMissingPropertiesFixer struct { + sourceFile *ast.SourceFile + typeChecker *checker.Checker +} + +type missingProperties struct { + declaration *ast.Node + properties []*ast.Symbol +} + +var addMissingPropertiesErrorCodes = []int32{ + diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.Code(), + diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.Code(), + diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.Code(), + diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.Code(), + diagnostics.Type_0_is_not_assignable_to_type_1.Code(), + diagnostics.Type_0_does_not_satisfy_the_expected_type_1.Code(), +} + +var AddMissingPropertiesFixProvider = &CodeFixProvider{ + ErrorCodes: addMissingPropertiesErrorCodes, + GetCodeActions: getCodeActionsToAddMissingProperties, + FixIds: []string{fixMissingProperties}, + GetAllCodeActions: getAllCodeActionsToAddMissingProperties, +} + +func getCodeActionsToAddMissingProperties(ctx context.Context, fixContext *CodeFixContext) ([]*CodeAction, error) { + typeChecker, done := fixContext.Program.GetTypeCheckerForFile(ctx, fixContext.SourceFile) + defer done() + + fix := addMissingPropertiesFixer{ + sourceFile: fixContext.SourceFile, + typeChecker: typeChecker, + } + + declaration, properties := fix.getInfo(fixContext.Span.Pos(), fixContext.ErrorCode) + if len(properties) == 0 { + return nil, nil + } + + tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + importAdder, err := fixContext.LS.createImportAdder(ctx, typeChecker, fixContext.SourceFile) + if err != nil { + return nil, err + } + + loc := locale.FromContext(ctx) + fixer := newMissingMemberFixer(tracker, fixContext.Program, typeChecker, fixContext.LS.UserPreferences(), importAdder, loc) + fix.addChanges(fixer, declaration, properties) + + fixer.addImports() + if importAdder != nil { + importAdder.WriteFixes(tracker) + } + changes := getCodeFixChanges(fixContext.SourceFile, tracker) + if len(changes) == 0 { + return nil, nil + } + + return []*CodeAction{{ + Description: diagnostics.Add_missing_properties.Localize(loc), + Changes: changes, + FixID: fixMissingProperties, + FixAllDescription: diagnostics.Add_all_missing_properties.Localize(loc), + }}, nil +} + +func getAllCodeActionsToAddMissingProperties(ctx context.Context, fixContext *CodeFixContext, fixAll *CodeFixAll) (*CombinedCodeActions, error) { + fix := addMissingPropertiesFixer{ + sourceFile: fixContext.SourceFile, + typeChecker: fixAll.typeChecker, + } + + var fixes []missingProperties + var seen collections.Set[*ast.Node] + for _, diagnostic := range fixAll.diagnostics { + if diagnostic.File() == fixContext.SourceFile && isFixableDiagnostic(diagnostic, addMissingPropertiesErrorCodes) { + declaration, properties := fix.getInfo(diagnostic.Pos(), diagnostic.Code()) + if len(properties) > 0 && seen.AddIfAbsent(declaration) { + fixes = append(fixes, missingProperties{declaration: declaration, properties: properties}) + } + } + } + + if len(fixes) == 0 { + return nil, nil + } + + importAdder, err := fixAll.getImportAdder(ctx, fixContext) + if err != nil { + return nil, err + } + + tracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) + loc := locale.FromContext(ctx) + fixer := newMissingMemberFixer(tracker, fixContext.Program, fixAll.typeChecker, fixContext.LS.UserPreferences(), importAdder, loc) + fix.addAllChanges(fixer, fixes) + + changes := getCodeFixChanges(fixContext.SourceFile, tracker) + if len(changes) == 0 { + return nil, nil + } + fixer.addImports() + + return &CombinedCodeActions{ + Description: diagnostics.Add_all_missing_properties.Localize(loc), + Changes: changes, + }, nil +} + +func (fix *addMissingPropertiesFixer) getInfo(pos int, errorCode int32) (*ast.Node, []*ast.Symbol) { + token := astnav.GetTokenAtPosition(fix.sourceFile, pos) + if token == nil || token.Parent == nil { + return nil, nil + } + + parent := token.Parent + if errorCode == diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.Code() || ast.IsObjectLiteralExpression(parent) && ast.IsCallExpression(parent.Parent) { + if !(token.Kind == ast.KindOpenBraceToken && ast.IsObjectLiteralExpression(parent) && ast.IsCallExpression(parent.Parent)) { + return nil, nil + } + argumentIndex := slices.Index(parent.Parent.Arguments(), parent) + if argumentIndex < 0 { + return nil, nil + } + signature := fix.typeChecker.GetResolvedSignature(parent.Parent) + if signature == nil || signature.Declaration() == nil || len(signature.Parameters()) == 0 { + return nil, nil + } + parameterIndex := argumentIndex + if signature.HasRestParameter() { + parameterIndex = min(parameterIndex, len(signature.Parameters())-1) + } + if parameterIndex >= len(signature.Parameters()) { + return nil, nil + } + parameter := signature.Parameters()[parameterIndex].ValueDeclaration() + if parameter == nil || !ast.IsParameterDeclaration(parameter) || !ast.IsIdentifier(parameter.Name()) { + return nil, nil + } + } + + var literal *ast.Node + var targetType *checker.Type + if token.Kind == ast.KindOpenBraceToken || ast.IsSatisfiesExpression(parent) || ast.IsReturnStatement(parent) { + literal = parent + if ast.IsSatisfiesExpression(parent) || ast.IsReturnStatement(parent) { + literal = parent.Expression() + } + if literal == nil || !ast.IsObjectLiteralExpression(literal) { + return nil, nil + } + if ast.IsSatisfiesExpression(parent) { + targetType = fix.typeChecker.GetTypeFromTypeNode(parent.Type()) + } else { + targetType = fix.typeChecker.GetContextualType(literal, checker.ContextFlagsNone) + } + } else if ast.IsIdentifier(token) && ast.HasInitializer(parent) && parent.Initializer() != nil && ast.IsObjectLiteralExpression(parent.Initializer()) { + literal = parent.Initializer() + targetType = core.OrElse(fix.typeChecker.GetContextualType(token, checker.ContextFlagsNone), fix.typeChecker.GetTypeAtLocation(token)) + } else { + return nil, nil + } + + if targetType == nil { + return nil, nil + } + + return literal, fix.typeChecker.GetUnmatchedProperties(fix.typeChecker.GetTypeAtLocation(literal), fix.typeChecker.GetNonNullableType(targetType), false /*requireOptionalProperties*/, false /*matchDiscriminantProperties*/) +} + +func (fix *addMissingPropertiesFixer) addChanges(f *missingMemberFixer, literal *ast.Node, properties []*ast.Symbol) { + factory := f.changeTracker.NodeFactory + quotePreference := lsutil.GetQuotePreference(fix.sourceFile, f.preferences) + members := append([]*ast.Node{}, literal.AsObjectLiteralExpression().Properties.Nodes...) + members = append(members, fix.createMissingProperties(f, literal, properties, quotePreference)...) + fix.replaceObjectLiteral(f, literal, factory.NewObjectLiteralExpression(factory.NewNodeList(members), true /*multiLine*/)) +} + +func (fix *addMissingPropertiesFixer) addAllChanges(f *missingMemberFixer, fixes []missingProperties) { + factory := f.changeTracker.NodeFactory + quotePreference := lsutil.GetQuotePreference(fix.sourceFile, f.preferences) + for _, info := range fixes { + members := fix.createMissingProperties(f, info.declaration, info.properties, quotePreference) + properties := info.declaration.AsObjectLiteralExpression().Properties + if len(properties.Nodes) == 0 { + fix.replaceObjectLiteral(f, info.declaration, factory.NewObjectLiteralExpression(factory.NewNodeList(members), true /*multiLine*/)) + } else { + f.changeTracker.InsertNodesInListAfter(fix.sourceFile, properties.Nodes[len(properties.Nodes)-1], members, properties) + } + } +} + +func (fix *addMissingPropertiesFixer) createMissingProperties(f *missingMemberFixer, literal *ast.Node, properties []*ast.Symbol, quotePreference lsutil.QuotePreference) []*ast.Node { + factory := f.changeTracker.NodeFactory + members := make([]*ast.Node, 0, len(properties)) + for _, property := range properties { + initializer := f.tryGetValueFromType(f.typeChecker.GetTypeOfSymbol(property), literal, fix.sourceFile, quotePreference, nil /*typeStack*/) + members = append(members, factory.NewPropertyAssignment(nil /*modifiers*/, f.createPropertyNameFromSymbol(property, literal, quotePreference), nil /*postfixToken*/, nil /*typeNode*/, initializer)) + } + return members +} + +func (fix *addMissingPropertiesFixer) replaceObjectLiteral(f *missingMemberFixer, literal *ast.Node, replacement *ast.Node) { + var options *change.NodeOptions + if ast.IsReturnStatement(literal.Parent) || ast.IsYieldExpression(literal.Parent) { + indentation := 0 + options = &change.NodeOptions{ + LeadingTriviaOption: change.LeadingTriviaOptionExclude, + TrailingTriviaOption: change.TrailingTriviaOptionExclude, + Indentation: &indentation, + } + } + + f.changeTracker.ReplaceNode(fix.sourceFile, literal, replacement, options) +} diff --git a/tsc/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go b/tsc/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go index b91cc1b1c44b4..7ed5073eda995 100644 --- a/tsc/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go +++ b/tsc/internal/ls/codeactions_fixclassincorrectlyimplementsinterface.go @@ -12,7 +12,6 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/ls/autoimport" "github.com/microsoft/TypeScript/tsc/internal/ls/change" - "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/scanner" ) @@ -45,13 +44,17 @@ func getCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Cont var actions []*CodeAction for _, implementedTypeNode := range implementsTypes { changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) - importAdder, err := createImportAdder(context, fixContext, typeChecker) + importAdder, err := fixContext.LS.createImportAdder(context, typeChecker, fixContext.SourceFile) if err != nil { return nil, err } - addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode) - changes := getChanges(changeTracker, importAdder, fixContext.SourceFile) + fixer := addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode) + fixer.addImports() + if importAdder != nil { + importAdder.WriteFixes(changeTracker) + } + changes := getCodeFixChanges(fixContext.SourceFile, changeTracker) if len(changes) == 0 { continue } @@ -66,39 +69,37 @@ func getCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Cont return actions, nil } -func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) { - allDiags := getAllDiagnostics(context, fixContext.Program, fixContext.SourceFile) - - typeChecker, done := fixContext.Program.GetTypeCheckerForFile(context, fixContext.SourceFile) - defer done() - +func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.Context, fixContext *CodeFixContext, fixAll *CodeFixAll) (*CombinedCodeActions, error) { changeTracker := change.NewTracker(context, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) - importAdder, err := createImportAdder(context, fixContext, typeChecker) - if err != nil { - return nil, err - } - seenClassDeclarations := collections.Set[*ast.Node]{} + var fixers []*missingMemberFixer - for _, diag := range allDiags { - if isFixableDiagnostic(diag, fixClassIncorrectlyImplementsInterfaceErrorCodes) { + for _, diag := range fixAll.diagnostics { + if diag.File() == fixContext.SourceFile && isFixableDiagnostic(diag, fixClassIncorrectlyImplementsInterfaceErrorCodes) { classDeclaration := getClass(fixContext.SourceFile, core.NewTextRange(diag.Pos(), diag.End())) if classDeclaration == nil { continue } if seenClassDeclarations.AddIfAbsent(classDeclaration) { + importAdder, err := fixAll.getImportAdder(context, fixContext) + if err != nil { + return nil, err + } implementsTypes := ast.GetImplementsHeritageClauseElements(classDeclaration) for _, implementedTypeNode := range implementsTypes { - addChanges(context, fixContext, changeTracker, importAdder, typeChecker, classDeclaration, implementedTypeNode) + fixers = append(fixers, addChanges(context, fixContext, changeTracker, importAdder, fixAll.typeChecker, classDeclaration, implementedTypeNode)) } } } } - changes := getChanges(changeTracker, importAdder, fixContext.SourceFile) + changes := getCodeFixChanges(fixContext.SourceFile, changeTracker) if len(changes) == 0 { return nil, nil } + for _, fixer := range fixers { + fixer.addImports() + } return &CombinedCodeActions{ Description: diagnostics.Implement_all_unimplemented_interfaces.Localize(locale.FromContext(context)), @@ -106,7 +107,7 @@ func getAllCodeActionsToFixClassIncorrectlyImplementsInterface(context context.C }, nil } -func addChanges(context context.Context, fixContext *CodeFixContext, changeTracker *change.Tracker, importAdder autoimport.ImportAdder, typeChecker *checker.Checker, classDeclaration *ast.Node, implementedTypeNode *ast.HeritageClauseElement) { +func addChanges(context context.Context, fixContext *CodeFixContext, changeTracker *change.Tracker, importAdder autoimport.ImportAdder, typeChecker *checker.Checker, classDeclaration *ast.Node, implementedTypeNode *ast.HeritageClauseElement) *missingMemberFixer { missingMemberFixer := newMissingMemberFixer(changeTracker, fixContext.Program, typeChecker, fixContext.LS.UserPreferences(), importAdder, locale.FromContext(context)) constructor := getConstructor(classDeclaration) implementedType := typeChecker.GetTypeAtLocation(implementedTypeNode) @@ -133,18 +134,7 @@ func addChanges(context context.Context, fixContext *CodeFixContext, changeTrack insertInterfaceMemberNode(changeTracker, fixContext.SourceFile, classDeclaration, constructor, memberNode) } } -} - -func getChanges(changeTracker *change.Tracker, importAdder autoimport.ImportAdder, sourceFile *ast.SourceFile) []*lsproto.TextEdit { - changes, unmappable := changeTracker.GetChanges() - if len(unmappable) != 0 { - return nil - } - fileChanges := changes[sourceFile.OriginalFileName()] - if importAdder != nil && importAdder.HasFixes() { - fileChanges = append(fileChanges, importAdder.Edits()...) - } - return fileChanges + return missingMemberFixer } func insertInterfaceMemberNode(changeTracker *change.Tracker, sourceFile *ast.SourceFile, classDeclaration *ast.Node, constructor *ast.Node, member *ast.Node) { @@ -229,14 +219,3 @@ func getInheritedMembers(typeChecker *checker.Checker, classDeclaration *ast.Nod } return inheritedMembers } - -func createImportAdder(context context.Context, fixContext *CodeFixContext, typeChecker *checker.Checker) (autoimport.ImportAdder, error) { - view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile, typeChecker) - if err != nil { - return nil, err - } - if view == nil { - return nil, nil - } - return autoimport.NewImportAdder(context, fixContext.Program, typeChecker, fixContext.SourceFile, view, fixContext.LS.FormatOptions(), fixContext.LS.converters, fixContext.LS.UserPreferences()), nil -} diff --git a/tsc/internal/ls/codeactions_fixmissingtypeannotation.go b/tsc/internal/ls/codeactions_fixmissingtypeannotation.go index 783f2207f78aa..ec0a3d4e0362b 100644 --- a/tsc/internal/ls/codeactions_fixmissingtypeannotation.go +++ b/tsc/internal/ls/codeactions_fixmissingtypeannotation.go @@ -125,44 +125,45 @@ func getIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *CodeFix return fixes, nil } -func getAllIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) { - allDiags := getAllDiagnostics(ctx, fixContext.Program, fixContext.SourceFile) - - ch, done := fixContext.Program.GetTypeCheckerForFile(ctx, fixContext.SourceFile) - defer done() - +func getAllIsolatedDeclarationsCodeActions(ctx context.Context, fixContext *CodeFixContext, fixAll *CodeFixAll) (*CombinedCodeActions, error) { changeTracker := change.NewTracker(ctx, fixContext.Program.Options(), fixContext.LS.FormatOptions(), fixContext.LS.converters) - fixer := &isolatedDeclarationsFixer{ sourceFile: fixContext.SourceFile, program: fixContext.Program, - checker: ch, + checker: fixAll.typeChecker, changeTracker: changeTracker, locale: locale.FromContext(ctx), fixedNodes: make(map[*ast.Node]bool), typePrintMode: typePrintModeFull, } - for _, diag := range allDiags { - if isFixableDiagnostic(diag, isolatedDeclarationsFixErrorCodes) { + for _, diag := range fixAll.diagnostics { + if diag.File() == fixContext.SourceFile && isFixableDiagnostic(diag, isolatedDeclarationsFixErrorCodes) { span := core.NewTextRange(diag.Loc().Pos(), diag.Loc().End()) fixer.addTypeAnnotation(span) } } - for _, sym := range fixer.symbolsToImport { - fixer.addSymbolToExistingImport(sym) + changes := getCodeFixChanges(fixContext.SourceFile, changeTracker) + if len(changes) == 0 { + return nil, nil } - changes, _ := changeTracker.GetChanges() - fileChanges := changes[fixContext.SourceFile.OriginalFileName()] - if len(fileChanges) == 0 { - return nil, nil + if len(fixer.symbolsToImport) > 0 { + importAdder, err := fixAll.getImportAdder(ctx, fixContext) + if err != nil { + return nil, err + } + if importAdder != nil { + for _, symbol := range fixer.symbolsToImport { + importAdder.AddImportFromExportedSymbol(symbol, true /*isValidTypeOnlyUseSite*/) + } + } } return &CombinedCodeActions{ Description: diagnostics.Add_all_missing_type_annotations.Localize(locale.FromContext(ctx)), - Changes: fileChanges, + Changes: changes, }, nil } @@ -193,14 +194,11 @@ func tryCodeAction(ctx context.Context, fixContext *CodeFixContext, ch *checker. fixer.addSymbolToExistingImport(sym) } - changes, _ := changeTracker.GetChanges() - fileChanges := changes[fixContext.SourceFile.OriginalFileName()] - // Add import edits if import adder has fixes if importAdder != nil && importAdder.HasFixes() { - fileChanges = append(fileChanges, importAdder.Edits()...) + importAdder.WriteFixes(changeTracker) } - + fileChanges := getCodeFixChanges(fixContext.SourceFile, changeTracker) if len(fileChanges) == 0 { return nil } diff --git a/tsc/internal/ls/codeactions_importfixes.go b/tsc/internal/ls/codeactions_importfixes.go index 0055dfc74e4c2..e3691bdb257c5 100644 --- a/tsc/internal/ls/codeactions_importfixes.go +++ b/tsc/internal/ls/codeactions_importfixes.go @@ -94,16 +94,14 @@ func getImportCodeActions(ctx context.Context, fixContext *CodeFixContext) ([]*C return actions, nil } -func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (*CombinedCodeActions, error) { +func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext, fixAll *CodeFixAll) (*CombinedCodeActions, error) { if fixContext.SourceFile.FileName().IsDynamic() { return nil, nil } - allDiagnostics := fixContext.Program.GetSemanticDiagnostics(ctx, fixContext.SourceFile) - var importDiags []*ast.Diagnostic - for _, diag := range allDiagnostics { - if isFixableDiagnostic(diag, importFixErrorCodes) { + for _, diag := range fixAll.diagnostics { + if diag.File() == fixContext.SourceFile && isFixableDiagnostic(diag, importFixErrorCodes) { importDiags = append(importDiags, diag) } } @@ -112,41 +110,20 @@ func getAllImportCodeActions(ctx context.Context, fixContext *CodeFixContext) (* return nil, nil } - ch, done := fixContext.Program.GetTypeChecker(ctx) - defer done() - - view, err := fixContext.LS.getPreparedAutoImportView(fixContext.SourceFile, ch) + importAdder, err := fixAll.getImportAdder(ctx, fixContext) if err != nil { return nil, err } - if view == nil { - view = fixContext.LS.getCurrentAutoImportView(fixContext.SourceFile, ch) - } - - importAdder := autoimport.NewImportAdder( - ctx, - fixContext.Program, - ch, - fixContext.SourceFile, - view, - fixContext.LS.FormatOptions(), - fixContext.LS.converters, - fixContext.LS.UserPreferences(), - ) for _, diag := range importDiags { - if err := addImportFromDiagnostic(ch, importAdder, diag, fixContext); err != nil { + err := addImportFromDiagnostic(fixAll.typeChecker, importAdder, diag, fixContext) + if err != nil { return nil, err } } - if !importAdder.HasFixes() { - return nil, nil - } - return &CombinedCodeActions{ Description: diagnostics.Add_all_missing_imports.Localize(locale.FromContext(ctx)), - Changes: importAdder.Edits(), }, nil } diff --git a/tsc/internal/ls/codeactions_missingmemberfixer.go b/tsc/internal/ls/codeactions_missingmemberfixer.go index 4f1cf6caf0a36..3efb39b1be6dc 100644 --- a/tsc/internal/ls/codeactions_missingmemberfixer.go +++ b/tsc/internal/ls/codeactions_missingmemberfixer.go @@ -1,18 +1,24 @@ package ls import ( + "slices" "strconv" + "strings" "github.com/microsoft/TypeScript/tsc/internal/ast" "github.com/microsoft/TypeScript/tsc/internal/checker" + "github.com/microsoft/TypeScript/tsc/internal/collections" "github.com/microsoft/TypeScript/tsc/internal/compiler" "github.com/microsoft/TypeScript/tsc/internal/core" "github.com/microsoft/TypeScript/tsc/internal/diagnostics" + "github.com/microsoft/TypeScript/tsc/internal/jsnum" "github.com/microsoft/TypeScript/tsc/internal/locale" "github.com/microsoft/TypeScript/tsc/internal/ls/autoimport" "github.com/microsoft/TypeScript/tsc/internal/ls/change" "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" + "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" "github.com/microsoft/TypeScript/tsc/internal/nodebuilder" + "github.com/microsoft/TypeScript/tsc/internal/scanner" ) type preserveOptionalFlags int @@ -24,12 +30,14 @@ const ( ) type missingMemberFixer struct { - changeTracker *change.Tracker - typeChecker *checker.Checker - program *compiler.Program - preferences lsutil.UserPreferences - importAdder autoimport.ImportAdder - locale locale.Locale + changeTracker *change.Tracker + typeChecker *checker.Checker + program *compiler.Program + preferences lsutil.UserPreferences + importAdder autoimport.ImportAdder + locale locale.Locale + symbolsToImport map[*ast.Symbol]bool + importsToPromote collections.Set[*ast.Node] } func newMissingMemberFixer(changeTracker *change.Tracker, program *compiler.Program, typeChecker *checker.Checker, preferences lsutil.UserPreferences, importAdder autoimport.ImportAdder, locale locale.Locale) *missingMemberFixer { @@ -260,7 +268,7 @@ func (f *missingMemberFixer) createSignatureDeclarationFromSignature(signature * } parameter := p.AsParameterDeclaration() - parameterTypeNode := parameter.Type + parameterTypeNode := core.IfElse(isJS, nil /*parameterTypeNode*/, parameter.Type) if parameterTypeNode != nil { parameterTypeNode = f.importTypeNode(parameterTypeNode, idToSymbol) } @@ -382,7 +390,7 @@ func (f *missingMemberFixer) importTypeNode(typeNode *ast.TypeNode, idToSymbol m if exportSymbol == nil { continue } - f.importAdder.AddImportFromExportedSymbol(exportSymbol, true /*isValidTypeOnlyUseSite*/) + f.addSymbolToImport(exportSymbol, true /*isValidTypeOnlyUseSite*/) } return importedTypeNode } @@ -397,11 +405,34 @@ func (f *missingMemberFixer) importTypeNode(typeNode *ast.TypeNode, idToSymbol m if exportSymbol == nil { continue } - f.importAdder.AddImportFromExportedSymbol(exportSymbol, true /*isValidTypeOnlyUseSite*/) + f.addSymbolToImport(exportSymbol, true /*isValidTypeOnlyUseSite*/) } return typeNode } +func (f *missingMemberFixer) addSymbolToImport(symbol *ast.Symbol, isValidTypeOnlyUseSite bool) { + if f.symbolsToImport == nil { + f.symbolsToImport = make(map[*ast.Symbol]bool) + } + previous, exists := f.symbolsToImport[symbol] + f.symbolsToImport[symbol] = isValidTypeOnlyUseSite && (!exists || previous) +} + +func (f *missingMemberFixer) addImports() { + if f.importAdder == nil { + return + } + for symbol, isValidTypeOnlyUseSite := range f.symbolsToImport { + f.importAdder.AddImportFromExportedSymbol(symbol, isValidTypeOnlyUseSite) + } + for declaration := range f.importsToPromote.Keys() { + f.importAdder.AddImportFix(&autoimport.Fix{ + AutoImportFix: &lsproto.AutoImportFix{Kind: lsproto.AutoImportFixKindPromoteTypeOnly}, + TypeOnlyAliasDeclaration: declaration, + }) + } +} + func (f *missingMemberFixer) getExportedSymbol(symbol *ast.Symbol) *ast.Symbol { symbol = f.typeChecker.GetExportSymbolOfSymbol(symbol) if symbol == nil || symbol.Parent() == nil { @@ -432,6 +463,10 @@ func (f *missingMemberFixer) createBody(body *ast.FunctionBody, quotePreference } func (f *missingMemberFixer) createStubbedMethodBody(quotePreference lsutil.QuotePreference) *ast.FunctionBody { + return f.createStubbedBody(quotePreference, diagnostics.Method_not_implemented.Localize(f.locale)) +} + +func (f *missingMemberFixer) createStubbedBody(quotePreference lsutil.QuotePreference, message string) *ast.FunctionBody { tokenFlags := ast.TokenFlagsNone if quotePreference == lsutil.QuotePreferenceSingle { tokenFlags = ast.TokenFlagsSingleQuote @@ -441,13 +476,179 @@ func (f *missingMemberFixer) createStubbedMethodBody(quotePreference lsutil.Quot f.changeTracker.NodeFactory.NewThrowStatement( f.changeTracker.NodeFactory.NewNewExpression( f.changeTracker.NodeFactory.NewIdentifier("Error"), nil /*typeArguments*/, f.changeTracker.NodeFactory.NewNodeList([]*ast.Node{ - f.changeTracker.NodeFactory.NewStringLiteral(diagnostics.Method_not_implemented.Localize(f.locale), tokenFlags), + f.changeTracker.NodeFactory.NewStringLiteral(message, tokenFlags), }), ), ), }), true /*multiLine*/) } +func (f *missingMemberFixer) createPropertyNameFromSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node, quotePreference lsutil.QuotePreference) *ast.Node { + factory := f.changeTracker.NodeFactory + if symbol.Flags()&ast.SymbolFlagsTransient != 0 { + nameType := f.typeChecker.GetNameTypeOfSymbol(symbol) + if nameType != nil && nameType.Flags()&(checker.TypeFlagsEnumLiteral|checker.TypeFlagsUniqueESSymbol) != 0 { + expression := f.createExpressionFromSymbol(nameType.Symbol(), enclosingDeclaration) + if expression != nil { + return factory.NewComputedPropertyName(expression) + } + } + builder := checker.NewNodeBuilder(f.typeChecker, f.changeTracker.EmitContext) + name := builder.SymbolToNode(symbol, ast.SymbolFlagsValue, enclosingDeclaration, nodebuilder.FlagsNone, nodebuilder.InternalFlagsWriteComputedProps, nil /*tracker*/) + if name != nil && ast.IsComputedPropertyName(name) { + nameSymbol := f.typeChecker.GetSymbolAtLocation(name.Expression()) + if nameSymbol != nil { + expression := f.createExpressionFromSymbol(nameSymbol, enclosingDeclaration) + if expression != nil { + return factory.NewComputedPropertyName(expression) + } + } + return factory.DeepCloneNode(name) + } + } + if scanner.IsIdentifierText(symbol.Name(), core.LanguageVariantStandard) { + return factory.NewIdentifier(symbol.Name()) + } + value := jsnum.FromString(symbol.Name()) + if value >= 0 && value.String() == symbol.Name() { + return factory.NewNumericLiteral(symbol.Name(), ast.TokenFlagsNone) + } + return factory.NewStringLiteral(symbol.Name(), core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone)) +} + +func (f *missingMemberFixer) createExpressionFromSymbol(symbol *ast.Symbol, enclosingDeclaration *ast.Node) *ast.Node { + builder, idToSymbol := f.createNodeBuilder() + expression := builder.SymbolToExpression(symbol, ast.SymbolFlagsValue, enclosingDeclaration, nodebuilder.FlagsUseFullyQualifiedType, nodebuilder.InternalFlagsNone, nil /*tracker*/) + if expression == nil || f.importAdder == nil { + return expression + } + + identifier := ast.GetLeftmostExpression(expression, true /*stopAtCallExpressions*/) + rootSymbol := idToSymbol[identifier] + if rootSymbol == nil { + return expression + } + declaration := f.typeChecker.GetTypeOnlyAliasDeclaration(rootSymbol) + if declaration != nil && ast.GetSourceFileOfNode(declaration) == ast.GetSourceFileOfNode(enclosingDeclaration) { + f.importsToPromote.Add(declaration) + } else { + resolvedSymbol := f.typeChecker.ResolveName(identifier.Text(), enclosingDeclaration, ast.SymbolFlagsValue, false /*excludeGlobals*/) + if resolvedSymbol != nil && f.typeChecker.GetMergedSymbol(f.typeChecker.SkipAlias(resolvedSymbol)) == f.typeChecker.GetMergedSymbol(f.typeChecker.SkipAlias(rootSymbol)) { + return expression + } + exportSymbol := f.getExportedSymbol(rootSymbol) + if exportSymbol != nil { + f.addSymbolToImport(exportSymbol, false /*isValidTypeOnlyUseSite*/) + } + } + return expression +} + +func (f *missingMemberFixer) tryGetValueFromType(t *checker.Type, enclosingDeclaration *ast.Node, sourceFile *ast.SourceFile, quotePreference lsutil.QuotePreference, typeStack []*checker.Type) *ast.Node { + factory := f.changeTracker.NodeFactory + if slices.Contains(typeStack, t) || f.typeChecker.IsDeeplyNestedType(t, typeStack, 3 /*maxDepth*/) { + return factory.NewIdentifier("undefined") + } + typeStack = append(typeStack, t) + tokenFlags := core.IfElse(quotePreference == lsutil.QuotePreferenceSingle, ast.TokenFlagsSingleQuote, ast.TokenFlagsNone) + flags := t.Flags() + switch { + case flags&checker.TypeFlagsAnyOrUnknown != 0: + return factory.NewIdentifier("undefined") + case flags&(checker.TypeFlagsString|checker.TypeFlagsTemplateLiteral) != 0: + return factory.NewStringLiteral("", tokenFlags) + case flags&checker.TypeFlagsNumber != 0: + return factory.NewNumericLiteral("0", ast.TokenFlagsNone) + case flags&checker.TypeFlagsBigInt != 0: + return factory.NewBigIntLiteral("0n", ast.TokenFlagsNone) + case flags&checker.TypeFlagsBoolean != 0: + return factory.NewKeywordExpression(ast.KindFalseKeyword) + case flags&checker.TypeFlagsEnumLike != 0: + member := t.Symbol() + if member.Flags()&ast.SymbolFlagsEnum != 0 { + member = nil + for _, declaration := range t.Symbol().Declarations() { + if ast.IsEnumDeclaration(declaration) && len(declaration.AsEnumDeclaration().Members.Nodes) > 0 { + member = f.typeChecker.GetSymbolOfDeclaration(declaration.AsEnumDeclaration().Members.Nodes[0]) + break + } + } + } + if member != nil { + expression := f.createExpressionFromSymbol(member, enclosingDeclaration) + if expression != nil { + return expression + } + } + return factory.NewNumericLiteral("0", ast.TokenFlagsNone) + case flags&checker.TypeFlagsStringLiteral != 0: + return factory.NewStringLiteral(t.AsLiteralType().Value().(string), tokenFlags) + case flags&(checker.TypeFlagsNumberLiteral|checker.TypeFlagsBigIntLiteral) != 0: + text := t.AsLiteralType().String() + negative := strings.HasPrefix(text, "-") + text = strings.TrimPrefix(text, "-") + var literal *ast.Node + if flags&checker.TypeFlagsBigIntLiteral != 0 { + literal = factory.NewBigIntLiteral(text, ast.TokenFlagsNone) + } else { + literal = factory.NewNumericLiteral(text, ast.TokenFlagsNone) + } + if negative { + return factory.NewPrefixUnaryExpression(ast.KindMinusToken, literal) + } + return literal + case flags&checker.TypeFlagsBooleanLiteral != 0: + return factory.NewKeywordExpression(core.IfElse(t.AsLiteralType().Value().(bool), ast.KindTrueKeyword, ast.KindFalseKeyword)) + case flags&checker.TypeFlagsNull != 0: + return factory.NewKeywordExpression(ast.KindNullKeyword) + case t.IsUnion(): + return f.tryGetValueFromType(t.Types()[0], enclosingDeclaration, sourceFile, quotePreference, typeStack) + case f.typeChecker.IsArrayLikeType(t): + return factory.NewArrayLiteralExpression(nil /*elements*/, false /*multiLine*/) + } + + symbol := t.Symbol() + if flags&checker.TypeFlagsObject != 0 && (t.ObjectFlags()&checker.ObjectFlagsObjectLiteral != 0 || symbol != nil && len(symbol.Declarations()) == 1 && ast.IsTypeLiteralNode(symbol.Declarations()[0])) { + var properties []*ast.Node + for _, property := range f.typeChecker.GetPropertiesOfType(t) { + initializer := f.tryGetValueFromType(f.typeChecker.GetTypeOfSymbol(property), enclosingDeclaration, sourceFile, quotePreference, typeStack) + properties = append(properties, factory.NewPropertyAssignment(nil /*modifiers*/, f.createPropertyNameFromSymbol(property, enclosingDeclaration, quotePreference), nil /*postfixToken*/, nil /*typeNode*/, initializer)) + } + return factory.NewObjectLiteralExpression(factory.NewNodeList(properties), true /*multiLine*/) + } + if t.ObjectFlags()&checker.ObjectFlagsAnonymous != 0 && symbol != nil { + for _, declaration := range symbol.Declarations() { + if ast.IsFunctionTypeNode(declaration) || declaration.Kind == ast.KindMethodSignature || ast.IsMethodDeclaration(declaration) { + signature := core.FirstOrNil(f.typeChecker.GetCallSignatures(t)) + if signature != nil { + body := f.createStubbedBody(quotePreference, diagnostics.Function_not_implemented.Localize(f.locale)) + function := f.createSignatureDeclarationFromSignature(signature, ast.KindFunctionExpression, sourceFile, enclosingDeclaration, body, nil /*modifiers*/, nil /*name*/, false /*optional*/) + if function != nil { + return function + } + } + break + } + } + } + if t.IsClass() { + declaration := ast.GetClassLikeDeclarationOfSymbol(symbol) + if declaration != nil && !ast.HasAbstractModifier(declaration) { + constructorType := f.typeChecker.GetTypeOfSymbol(symbol) + signatures := f.typeChecker.GetSignaturesOfType(constructorType, checker.SignatureKindConstruct) + if f.typeChecker.IsConstructorAccessible(enclosingDeclaration, signatures) && core.Some(signatures, func(signature *checker.Signature) bool { + return f.typeChecker.GetMinArgumentCount(signature) == 0 + }) { + expression := f.createExpressionFromSymbol(symbol, enclosingDeclaration) + if expression != nil { + return factory.NewNewExpression(expression, nil /*typeArguments*/, nil /*arguments*/) + } + } + } + } + return factory.NewIdentifier("undefined") +} + func createDummyParameters(factory *ast.NodeFactory, argCount int, names []string, types []*ast.TypeNode, minArgumentCount int, inJS bool) *ast.ParameterList { parameters := make([]*ast.Node, 0, argCount) parameterNameCounts := make(map[string]int) diff --git a/tsc/internal/ls/completions.go b/tsc/internal/ls/completions.go index 2bfa4fb9faba9..1156e3cd3f2b7 100644 --- a/tsc/internal/ls/completions.go +++ b/tsc/internal/ls/completions.go @@ -2662,9 +2662,16 @@ func (l *LanguageService) getEntryForMemberCompletion(ctx context.Context, typeC } nodes := fixer.createMemberFromSymbol(symbol, classLikeDeclaration, file, body, preserveOptionalFlagsProperty, abstract) + fixer.addImports() var additionalTextEdits []*lsproto.TextEdit if importAdder != nil && importAdder.HasFixes() { - additionalTextEdits = importAdder.Edits() + importTracker := change.NewTracker(ctx, l.GetProgram().Options(), l.FormatOptions(), l.converters) + importAdder.WriteFixes(importTracker) + changes, unmappable := importTracker.GetChanges() + if len(unmappable) != 0 { + return nil, nil + } + additionalTextEdits = changes[file.OriginalFileName()] } if presentModifiers.eraseRange != nil { additionalTextEdits = append(additionalTextEdits, &lsproto.TextEdit{ @@ -6697,7 +6704,14 @@ func (l *LanguageService) getExhaustiveCaseSnippets( var additionalTextEdits *[]*lsproto.TextEdit if importAdder != nil { - if edits := importAdder.Edits(); len(edits) != 0 { + importTracker := change.NewTracker(ctx, options, l.FormatOptions(), l.converters) + importAdder.WriteFixes(importTracker) + changes, unmappable := importTracker.GetChanges() + if len(unmappable) != 0 { + return nil, nil + } + edits := changes[file.OriginalFileName()] + if len(edits) != 0 { additionalTextEdits = &edits } } diff --git a/tsc/internal/testutil/contentmappertest/editing.go b/tsc/internal/testutil/contentmappertest/editing.go index d295837a46bbe..152966b152353 100644 --- a/tsc/internal/testutil/contentmappertest/editing.go +++ b/tsc/internal/testutil/contentmappertest/editing.go @@ -23,7 +23,10 @@ func (prefixedSupplementalHandler) HandleRequest(ctx context.Context, method str if err := json.Unmarshal(params, &p); err != nil { return nil, err } - const prefix = "/* generated */\n" + prefix := "/* generated */\n" + if strings.Contains(p.FileName, "codefix-unmapped-import") { + prefix = "import type { C } from \"./a\";\n" + } features := spanmap.FeatureAll if strings.Contains(p.FileName, "folding-disabled") || strings.Contains(p.FileName, "codelens-disabled") || strings.Contains(p.FileName, "formatting-disabled") { features = spanmap.FeatureNone