diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index f7a2ce5e13177..b4fe921be1f5d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -83,6 +83,8 @@ namespace ts { getSignaturesOfType, getIndexTypeOfType, getBaseTypes, + getBaseTypeOfLiteralType, + getWidenedType, getTypeFromTypeNode, getParameterType: getTypeAtPosition, getReturnTypeOfSignature, diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3be35036144fc..db4bf23d78583 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3315,6 +3315,14 @@ "category": "Message", "code": 90015 }, + "Add declaration for missing property '{0}'": { + "category": "Message", + "code": 90016 + }, + "Add index signature for missing property '{0}'": { + "category": "Message", + "code": 90017 + }, "Octal literal types must use ES2015 syntax. Use the syntax '{0}'.": { "category": "Error", "code": 8017 diff --git a/src/compiler/types.ts b/src/compiler/types.ts index a48a6367bd024..7fc77b487088c 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2388,6 +2388,8 @@ getSignaturesOfType(type: Type, kind: SignatureKind): Signature[]; getIndexTypeOfType(type: Type, kind: IndexKind): Type; getBaseTypes(type: InterfaceType): BaseType[]; + getBaseTypeOfLiteralType(type: Type): Type; + getWidenedType(type: Type): Type; getReturnTypeOfSignature(signature: Signature): Type; /** * Gets the type of a parameter at a given position in a signature. diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 34aa9580d6d78..49d1336c87464 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -2122,7 +2122,7 @@ namespace FourSlash { * Because codefixes are only applied on the working file, it is unsafe * to apply this more than once (consider a refactoring across files). */ - public verifyRangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean) { + public verifyRangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number) { const ranges = this.getRanges(); if (ranges.length !== 1) { this.raiseError("Exactly one range should be specified in the testfile."); @@ -2130,7 +2130,7 @@ namespace FourSlash { const fileName = this.activeFile.fileName; - this.applyCodeFixActions(fileName, this.getCodeFixActions(fileName, errorCode)); + this.applyCodeAction(fileName, this.getCodeFixActions(fileName, errorCode), index); const actualText = this.rangeText(ranges[0]); @@ -2155,7 +2155,7 @@ namespace FourSlash { public verifyFileAfterCodeFix(expectedContents: string, fileName?: string) { fileName = fileName ? fileName : this.activeFile.fileName; - this.applyCodeFixActions(fileName, this.getCodeFixActions(fileName)); + this.applyCodeAction(fileName, this.getCodeFixActions(fileName)); const actualContents: string = this.getFileContent(fileName); if (this.removeWhitespace(actualContents) !== this.removeWhitespace(expectedContents)) { @@ -2193,12 +2193,20 @@ namespace FourSlash { return actions; } - private applyCodeFixActions(fileName: string, actions: ts.CodeAction[]): void { - if (!(actions && actions.length === 1)) { - this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`); + private applyCodeAction(fileName: string, actions: ts.CodeAction[], index?: number): void { + if (index === undefined) { + if (!(actions && actions.length === 1)) { + this.raiseError(`Should find exactly one codefix, but ${actions ? actions.length : "none"} found.`); + } + index = 0; + } + else { + if (!(actions && actions.length >= index + 1)) { + this.raiseError(`Should find at least ${index + 1} codefix(es), but ${actions ? actions.length : "none"} found.`); + } } - const fileChanges = ts.find(actions[0].changes, change => change.fileName === fileName); + const fileChanges = ts.find(actions[index].changes, change => change.fileName === fileName); if (!fileChanges) { this.raiseError("The CodeFix found doesn't provide any changes in this file."); } @@ -3535,8 +3543,8 @@ namespace FourSlashInterface { this.DocCommentTemplate(/*expectedText*/ undefined, /*expectedOffset*/ undefined, /*empty*/ true); } - public rangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean): void { - this.state.verifyRangeAfterCodeFix(expectedText, errorCode, includeWhiteSpace); + public rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void { + this.state.verifyRangeAfterCodeFix(expectedText, includeWhiteSpace, errorCode, index); } public importFixAtPosition(expectedTextArray: string[], errorCode?: number): void { diff --git a/src/services/codefixes/fixAddMissingMember.ts b/src/services/codefixes/fixAddMissingMember.ts new file mode 100644 index 0000000000000..6ae2ba3f51c99 --- /dev/null +++ b/src/services/codefixes/fixAddMissingMember.ts @@ -0,0 +1,67 @@ +/* @internal */ +namespace ts.codefix { + registerCodeFix({ + errorCodes: [Diagnostics.Property_0_does_not_exist_on_type_1.code], + getCodeActions: getActionsForAddMissingMember + }); + + function getActionsForAddMissingMember(context: CodeFixContext): CodeAction[] | undefined { + + const sourceFile = context.sourceFile; + const start = context.span.start; + // This is the identifier of the missing property. eg: + // this.missing = 1; + // ^^^^^^^ + const token = getTokenAtPosition(sourceFile, start); + + if (token.kind != SyntaxKind.Identifier) { + return undefined; + } + + const classDeclaration = getContainingClass(token); + if (!classDeclaration) { + return undefined; + } + + if (!(token.parent && token.parent.kind === SyntaxKind.PropertyAccessExpression)) { + return undefined; + } + + if ((token.parent as PropertyAccessExpression).expression.kind !== SyntaxKind.ThisKeyword) { + return undefined; + } + + let typeString = "any"; + + if (token.parent.parent.kind === SyntaxKind.BinaryExpression) { + const binaryExpression = token.parent.parent as BinaryExpression; + + const checker = context.program.getTypeChecker(); + const widenedType = checker.getWidenedType(checker.getBaseTypeOfLiteralType(checker.getTypeAtLocation(binaryExpression.right))); + typeString = checker.typeToString(widenedType); + } + + const startPos = classDeclaration.members.pos; + + return [{ + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_declaration_for_missing_property_0), [token.getText()]), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start: startPos, length: 0 }, + newText: `${token.getFullText(sourceFile)}: ${typeString};` + }] + }] + }, + { + description: formatStringFromArgs(getLocaleSpecificMessage(Diagnostics.Add_index_signature_for_missing_property_0), [token.getText()]), + changes: [{ + fileName: sourceFile.fileName, + textChanges: [{ + span: { start: startPos, length: 0 }, + newText: `[name: string]: ${typeString};` + }] + }] + }]; + } +} \ No newline at end of file diff --git a/src/services/codefixes/fixes.ts b/src/services/codefixes/fixes.ts index 3bd173e04f604..76be34c67cda5 100644 --- a/src/services/codefixes/fixes.ts +++ b/src/services/codefixes/fixes.ts @@ -1,4 +1,5 @@ /// +/// /// /// /// diff --git a/src/services/codefixes/helpers.ts b/src/services/codefixes/helpers.ts index 7bee4f5a0ed91..3eab994f84ce5 100644 --- a/src/services/codefixes/helpers.ts +++ b/src/services/codefixes/helpers.ts @@ -32,7 +32,7 @@ namespace ts.codefix { const declaration = declarations[0] as Declaration; const name = declaration.name ? declaration.name.getText() : undefined; - const visibility = getVisibilityPrefix(getModifierFlags(declaration)); + const visibility = getVisibilityPrefixWithSpace(getModifierFlags(declaration)); switch (declaration.kind) { case SyntaxKind.GetAccessor: @@ -58,7 +58,7 @@ namespace ts.codefix { if (declarations.length === 1) { Debug.assert(signatures.length === 1); const sigString = checker.signatureToString(signatures[0], enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; + return getStubbedMethod(visibility, name, sigString, newlineChar); } let result = ""; @@ -78,7 +78,7 @@ namespace ts.codefix { bodySig = createBodySignatureWithAnyTypes(signatures, enclosingDeclaration, checker); } const sigString = checker.signatureToString(bodySig, enclosingDeclaration, TypeFormatFlags.SuppressAnyReturnType, SignatureKind.Call); - result += `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; + result += getStubbedMethod(visibility, name, sigString, newlineChar); return result; default: @@ -138,11 +138,15 @@ namespace ts.codefix { } } - function getMethodBodyStub(newLineChar: string) { - return ` {${newLineChar}throw new Error('Method not implemented.');${newLineChar}}${newLineChar}`; + export function getStubbedMethod(visibility: string, name: string, sigString = "()", newlineChar: string): string { + return `${visibility}${name}${sigString}${getMethodBodyStub(newlineChar)}`; } - function getVisibilityPrefix(flags: ModifierFlags): string { + function getMethodBodyStub(newlineChar: string) { + return ` {${newlineChar}throw new Error('Method not implemented.');${newlineChar}}${newlineChar}`; + } + + function getVisibilityPrefixWithSpace(flags: ModifierFlags): string { if (flags & ModifierFlags.Public) { return "public "; } diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index b4e8289f367bd..d88cf137afa8b 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -78,6 +78,7 @@ "formatting/smartIndenter.ts", "formatting/tokenRange.ts", "codeFixProvider.ts", + "codefixes/fixAddMissingMember.ts", "codefixes/fixExtendsInterfaceBecomesImplements.ts", "codefixes/fixClassIncorrectlyImplementsInterface.ts", "codefixes/fixClassDoesntImplementInheritedAbstractMember.ts", diff --git a/tests/cases/fourslash/codeFixUndeclaredClassInstance.ts b/tests/cases/fourslash/codeFixUndeclaredClassInstance.ts new file mode 100644 index 0000000000000..024ec144092c8 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredClassInstance.ts @@ -0,0 +1,22 @@ +/// + +//// class A { +//// a: number; +//// b: string; +//// constructor(public x: any) {} +//// } +//// [|class B { +//// constructor() { +//// this.x = new A(3); +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class B { + x: A; + + constructor() { + this.x = new A(3); + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts b/tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts new file mode 100644 index 0000000000000..34e359feea89f --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredClassInstanceWithTypeParams.ts @@ -0,0 +1,22 @@ +/// + +//// class A { +//// a: number; +//// b: string; +//// constructor(public x: T) {} +//// } +//// [|class B { +//// constructor() { +//// this.x = new A(3); +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class B { + x: A; + + constructor() { + this.x = new A(3); + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts new file mode 100644 index 0000000000000..2e49a8184eb73 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredIndexSignatureNumericLiteral.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = 10; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + [name: string]: number; + + constructor() { + this.x = 10; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 1); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts new file mode 100644 index 0000000000000..6f0a5de3557b1 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionEmptyClass.ts @@ -0,0 +1,20 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = function(x: number, y?: A){ +//// return x > 0 ? x : y; +//// } +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: (x: number, y?: A) => A; + constructor() { + this.x = function(x: number, y?: A){ + return x > 0 ? x : y; + } + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts new file mode 100644 index 0000000000000..b05fb0d42b568 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyFunctionNonEmptyClass.ts @@ -0,0 +1,22 @@ +/// + +//// [|class A { +//// y: number; +//// constructor(public a: number) { +//// this.x = function(x: number, y?: A){ +//// return x > 0 ? x : y; +//// } +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: (x: number, y?: A) => number | A; + y: number; + constructor(public a: number) { + this.x = function(x: number, y?: A){ + return x > 0 ? x : y; + } + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts new file mode 100644 index 0000000000000..eaae355904b16 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyNumericLiteral.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.x = 10; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: number; + + constructor() { + this.x = 10; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts new file mode 100644 index 0000000000000..a2647c5a21e33 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteral.ts @@ -0,0 +1,19 @@ +/// + +//// [|class A { +//// constructor() { +//// let e: any = 10; +//// this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: { a: number; b: string; c: any; d: any; e: any; }; + + constructor() { + let e: any = 10; + this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts new file mode 100644 index 0000000000000..0b0ab8cdd59a2 --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyObjectLiteralStrictNullChecks.ts @@ -0,0 +1,21 @@ +/// + +// @strictNullChecks: true + +//// [|class A { +//// constructor() { +//// let e: any = 10; +//// this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + x: { a: number; b: string; c: undefined; d: null; e: any; }; + + constructor() { + let e: any = 10; + this.x = { a: 10, b: "hello", c: undefined, d: null, e: e }; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); \ No newline at end of file diff --git a/tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts b/tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts new file mode 100644 index 0000000000000..56f9b34d809ce --- /dev/null +++ b/tests/cases/fourslash/codeFixUndeclaredPropertyThisType.ts @@ -0,0 +1,17 @@ +/// + +//// [|class A { +//// constructor() { +//// this.mythis = this; +//// } +//// }|] + +verify.rangeAfterCodeFix(` +class A { + mythis: this; + + constructor() { + this.mythis = this; + } +} +`, /*includeWhiteSpace*/ false, /*errorCode*/ undefined, /*index*/ 0); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index 661d7cba6a495..34afb71ec85ea 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -225,7 +225,7 @@ declare namespace FourSlashInterface { noMatchingBracePositionInCurrentFile(bracePosition: number): void; DocCommentTemplate(expectedText: string, expectedOffset: number, empty?: boolean): void; noDocCommentTemplate(): void; - rangeAfterCodeFix(expectedText: string, errorCode?: number, includeWhiteSpace?: boolean): void; + rangeAfterCodeFix(expectedText: string, includeWhiteSpace?: boolean, errorCode?: number, index?: number): void; importFixAtPosition(expectedTextArray: string[], errorCode?: number): void; navigationBar(json: any): void; diff --git a/tests/cases/fourslash/unusedImports2FS.ts b/tests/cases/fourslash/unusedImports2FS.ts index 5387569336247..8865dea56a671 100644 --- a/tests/cases/fourslash/unusedImports2FS.ts +++ b/tests/cases/fourslash/unusedImports2FS.ts @@ -16,4 +16,4 @@ //// //// } -verify.rangeAfterCodeFix(`import {Calculator} from "./file1"`, /*errorCode*/ undefined, /*includeWhiteSpace*/ true); +verify.rangeAfterCodeFix(`import {Calculator} from "./file1"`, /*includeWhiteSpace*/ true, /*errorCode*/ undefined); diff --git a/tests/cases/fourslash/unusedLocalsInFunction3.ts b/tests/cases/fourslash/unusedLocalsInFunction3.ts index 7aa9b20a33ce3..0164873acd87e 100644 --- a/tests/cases/fourslash/unusedLocalsInFunction3.ts +++ b/tests/cases/fourslash/unusedLocalsInFunction3.ts @@ -7,4 +7,4 @@ //// z+1; ////} -verify.rangeAfterCodeFix("var x,z = 1;", 6133); +verify.rangeAfterCodeFix("var x,z = 1;", /*includeWhiteSpace*/ undefined, 6133);